mirror of
https://gitee.com/hhyykk/ipms-sjy-ui.git
synced 2025-02-01 19:24:58 +08:00
【代码评审】Mall:客服的会话列表
This commit is contained in:
parent
6550983413
commit
ec61670b75
@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<el-container v-if="showChatBox" class="kefu">
|
||||
<el-header>
|
||||
<!-- TODO @puhui999:keFuConversation => conversation -->
|
||||
<div class="kefu-title">{{ keFuConversation.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"
|
||||
@ -13,6 +16,7 @@
|
||||
</div>
|
||||
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)" @scroll="handleScroll">
|
||||
<div 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">
|
||||
<!-- 日期 -->
|
||||
@ -118,8 +122,10 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
defineOptions({ name: 'KeFuMessageBox' })
|
||||
|
||||
const message = ref('') // 消息弹窗
|
||||
|
||||
const messageTool = useMessage()
|
||||
const message = ref('') // 消息
|
||||
const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
|
||||
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
|
||||
const showNewMessageTip = ref(false) // 显示有新消息提示
|
||||
@ -128,7 +134,8 @@ const queryParams = reactive({
|
||||
conversationId: 0
|
||||
})
|
||||
const total = ref(0) // 消息总条数
|
||||
// 获得消息
|
||||
|
||||
/** 获得消息列表 */
|
||||
const getMessageList = async (conversation: KeFuConversationRespVO) => {
|
||||
keFuConversation.value = conversation
|
||||
queryParams.conversationId = conversation.id
|
||||
@ -146,12 +153,14 @@ const getMessageList = async (conversation: KeFuConversationRespVO) => {
|
||||
}
|
||||
await scrollToBottom()
|
||||
}
|
||||
|
||||
/** 按照时间倒序,获取消息列表 */
|
||||
const getMessageList0 = computed(() => {
|
||||
messageList.value.sort((a: any, b: any) => a.createTime - b.createTime)
|
||||
return messageList.value
|
||||
})
|
||||
|
||||
// 刷新消息列表
|
||||
/** 刷新消息列表 */
|
||||
const refreshMessageList = async () => {
|
||||
if (!keFuConversation.value) {
|
||||
return
|
||||
@ -164,14 +173,16 @@ const refreshMessageList = async () => {
|
||||
showNewMessageTip.value = true
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ getMessageList, refreshMessageList })
|
||||
// 是否显示聊天区域
|
||||
const showChatBox = computed(() => !isEmpty(keFuConversation.value))
|
||||
// 处理表情选择
|
||||
const showChatBox = computed(() => !isEmpty(keFuConversation.value)) // 是否显示聊天区域
|
||||
|
||||
/** 处理表情选择 */
|
||||
const handleEmojiSelect = (item: Emoji) => {
|
||||
message.value += item.name
|
||||
}
|
||||
// 处理图片发送
|
||||
|
||||
/** 处理图片发送 */
|
||||
const handleSendPicture = async (picUrl: string) => {
|
||||
// 组织发送消息
|
||||
const msg = {
|
||||
@ -181,7 +192,8 @@ const handleSendPicture = async (picUrl: string) => {
|
||||
}
|
||||
await sendMessage(msg)
|
||||
}
|
||||
// 发送消息
|
||||
|
||||
/** 发送文本消息 */
|
||||
const handleSendMessage = async () => {
|
||||
// 1. 校验消息是否为空
|
||||
if (isEmpty(unref(message.value))) {
|
||||
@ -197,7 +209,7 @@ const handleSendMessage = async () => {
|
||||
await sendMessage(msg)
|
||||
}
|
||||
|
||||
// 发送消息 【共用】
|
||||
/** 真正发送消息 【共用】*/
|
||||
const sendMessage = async (msg: any) => {
|
||||
// 发送消息
|
||||
await KeFuMessageApi.sendKeFuMessage(msg)
|
||||
@ -208,9 +220,9 @@ const sendMessage = async (msg: any) => {
|
||||
await scrollToBottom()
|
||||
}
|
||||
|
||||
/** 滚动到底部 */
|
||||
const innerRef = ref<HTMLDivElement>()
|
||||
const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
|
||||
// 滚动到底部
|
||||
const scrollToBottom = async () => {
|
||||
// 1. 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动
|
||||
if (loadHistory.value) {
|
||||
@ -223,12 +235,14 @@ const scrollToBottom = async () => {
|
||||
// 2.2 消息已读
|
||||
await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id)
|
||||
}
|
||||
// 查看新消息
|
||||
|
||||
/** 查看新消息 */
|
||||
const handleToNewMessage = async () => {
|
||||
loadHistory.value = false
|
||||
await scrollToBottom()
|
||||
}
|
||||
|
||||
/** 加载历史消息 */
|
||||
const loadingMore = ref(false) // 滚动到顶部加载更多
|
||||
const loadHistory = ref(false) // 加载历史消息
|
||||
const handleScroll = async ({ scrollTop }) => {
|
||||
@ -247,8 +261,10 @@ const handleOldMessage = async () => {
|
||||
loadingMore.value = false
|
||||
// TODO puhui999: 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示时间
|
||||
*
|
||||
* @param {*} item - 数据
|
||||
* @param {*} index - 索引
|
||||
*/
|
||||
|
@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="kefu">
|
||||
<!-- TODO @puhui999:item => conversation 会不会更容易理解 -->
|
||||
<div
|
||||
v-for="(item, index) in conversationList"
|
||||
:key="item.id"
|
||||
|
@ -10,6 +10,7 @@
|
||||
: ''
|
||||
]"
|
||||
>
|
||||
<!-- TODO @puhui999:unocss -->
|
||||
<el-image
|
||||
:src="message.content"
|
||||
fit="contain"
|
||||
@ -30,6 +31,7 @@ defineOptions({ name: 'ImageMessageItem' })
|
||||
defineProps<{
|
||||
message: KeFuMessageRespVO
|
||||
}>()
|
||||
|
||||
/** 图预览 */
|
||||
const imagePreview = (imgUrl: string) => {
|
||||
createImageViewer({
|
||||
|
@ -18,6 +18,7 @@
|
||||
</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"
|
||||
@ -29,10 +30,10 @@
|
||||
<div class="pay-box mt-30px flex justify-end pr-20px">
|
||||
<div class="flex items-center">
|
||||
<div class="discounts-title pay-color"
|
||||
>共 {{ getMessageContent.productCount }} 件商品,总金额:
|
||||
>共 {{ getMessageContent?.productCount }} 件商品,总金额:
|
||||
</div>
|
||||
<div class="discounts-money pay-color">
|
||||
¥{{ fenToYuan(getMessageContent.payPrice) }}
|
||||
¥{{ fenToYuan(getMessageContent?.payPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -90,6 +90,8 @@ const props = defineProps({
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
/** SKU 展示字符串 */
|
||||
const skuString = computed(() => {
|
||||
if (!props.skuText) {
|
||||
return ''
|
||||
@ -99,6 +101,8 @@ const skuString = computed(() => {
|
||||
}
|
||||
return props.skuText
|
||||
})
|
||||
|
||||
// TODO @puhui999:可以使用 preview-teleported
|
||||
/** 图预览 */
|
||||
const imagePrediv = (imgUrl: string) => {
|
||||
createImageViewer({
|
||||
|
@ -32,5 +32,7 @@ defineOptions({ name: 'ProductMessageItem' })
|
||||
const props = defineProps<{
|
||||
message: KeFuMessageRespVO
|
||||
}>()
|
||||
|
||||
/** 获悉消息内容 */
|
||||
const getMessageContent = computed(() => JSON.parse(props.message.content))
|
||||
</script>
|
||||
|
@ -17,6 +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" />
|
||||
</li>
|
||||
</ul>
|
||||
@ -31,6 +32,7 @@ import { Emoji, useEmoji } from './emoji'
|
||||
const { getEmojiList } = useEmoji()
|
||||
const emojiList = computed(() => getEmojiList())
|
||||
|
||||
/** 选择 emoji 表情 */
|
||||
const emits = defineEmits<{
|
||||
(e: 'select-emoji', v: Emoji)
|
||||
}>()
|
||||
|
@ -1,19 +1,24 @@
|
||||
<!-- 图片选择 -->
|
||||
<template>
|
||||
<div>
|
||||
<!-- TODO @puhui999:unocss -->
|
||||
<img :src="Picture" style="width: 35px; height: 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 * as FileApi from '@/api/infra/file'
|
||||
|
||||
defineOptions({ name: 'PictureSelectUpload' })
|
||||
const message = useMessage()
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
/** 选择并上传文件 */
|
||||
const emits = defineEmits<{
|
||||
(e: 'send-picture', v: string): void
|
||||
}>()
|
||||
// 选择并上传文件
|
||||
const selectAndUpload = async () => {
|
||||
const files: any = await getFiles()
|
||||
message.success('图片发送中请稍等。。。')
|
||||
@ -23,6 +28,7 @@ const selectAndUpload = async () => {
|
||||
|
||||
/**
|
||||
* 唤起文件选择窗口,并获取选择的文件
|
||||
*
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {boolean} [options.multiple=true] - 是否支持多选
|
||||
* @param {string} [options.accept=''] - 文件上传格式限制
|
||||
@ -54,7 +60,7 @@ async function getFiles(options = {}) {
|
||||
|
||||
// 等待文件选择元素的 change 事件
|
||||
try {
|
||||
const files = await new Promise((resolve, reject) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
input.addEventListener('change', (event: any) => {
|
||||
const filesArray = Array.from(event?.target?.files || [])
|
||||
|
||||
@ -68,9 +74,9 @@ async function getFiles(options = {}) {
|
||||
}
|
||||
|
||||
// 判断是否超出上传文件大小限制
|
||||
const oversizedFiles = filesArray.filter((file: File) => file.size / 1024 ** 2 > fileSize)
|
||||
if (oversizedFiles.length > 0) {
|
||||
reject({ errorType: 'fileSize', files: oversizedFiles })
|
||||
const overSizedFiles = filesArray.filter((file: File) => file.size / 1024 ** 2 > fileSize)
|
||||
if (overSizedFiles.length > 0) {
|
||||
reject({ errorType: 'fileSize', files: overSizedFiles })
|
||||
return
|
||||
}
|
||||
|
||||
@ -79,8 +85,6 @@ async function getFiles(options = {}) {
|
||||
resolve(fileList)
|
||||
})
|
||||
})
|
||||
|
||||
return files
|
||||
} catch (error) {
|
||||
console.error('选择文件出错:', error)
|
||||
throw error
|
||||
|
@ -58,8 +58,11 @@ export interface Emoji {
|
||||
|
||||
export const useEmoji = () => {
|
||||
const emojiPathList = ref<any[]>([])
|
||||
// 加载本地图片
|
||||
|
||||
// TODO @puhui999:initStaticEmoji 会不会更好
|
||||
/** 加载本地图片 */
|
||||
const getStaticEmojiPath = async () => {
|
||||
// TODO @puhui999:images 改成 asserts 更合适哈。
|
||||
const pathList = import.meta.glob(
|
||||
'@/views/mall/promotion/kefu/components/images/*.{png,jpg,jpeg,svg}'
|
||||
)
|
||||
@ -68,17 +71,25 @@ export const useEmoji = () => {
|
||||
emojiPathList.value.push(imageModule.default)
|
||||
}
|
||||
}
|
||||
// 初始化
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
if (isEmpty(emojiPathList.value)) {
|
||||
await getStaticEmojiPath()
|
||||
}
|
||||
})
|
||||
|
||||
// 处理表情
|
||||
// TODO @puhui999:建议 function 都改成 const 这种来定义哈。保持统一风格
|
||||
/**
|
||||
* 将文本中的表情替换成图片
|
||||
*
|
||||
* @param data 文本 TODO @puhui999:data => content
|
||||
* @return 替换后的文本
|
||||
*/
|
||||
function replaceEmoji(data: string) {
|
||||
let newData = data
|
||||
if (typeof newData !== 'object') {
|
||||
// TODO @puhui999: \] 是不是可以简化成 ]。我看 idea 提示了哈
|
||||
const reg = /\[(.+?)\]/g // [] 中括号
|
||||
const zhEmojiName = newData.match(reg)
|
||||
if (zhEmojiName) {
|
||||
@ -94,7 +105,11 @@ export const useEmoji = () => {
|
||||
return newData
|
||||
}
|
||||
|
||||
// 获得所有表情
|
||||
/**
|
||||
* 获得所有表情
|
||||
*
|
||||
* @return 表情列表
|
||||
*/
|
||||
function getEmojiList(): Emoji[] {
|
||||
return emojiList.map((item) => ({
|
||||
url: selEmojiFile(item.name),
|
||||
@ -102,6 +117,7 @@ export const useEmoji = () => {
|
||||
})) as Emoji[]
|
||||
}
|
||||
|
||||
// TODO @puhui999:getEmojiFileByName 会不会更容易理解哈
|
||||
function selEmojiFile(name: string) {
|
||||
for (const emoji of emojiList) {
|
||||
if (emoji.name === name) {
|
||||
|
Loading…
Reference in New Issue
Block a user