【代码评审】Mall:客服的会话列表

This commit is contained in:
YunaiV 2024-07-10 09:30:03 +08:00
parent 6550983413
commit ec61670b75
9 changed files with 72 additions and 26 deletions

View File

@ -1,9 +1,12 @@
<template> <template>
<el-container v-if="showChatBox" class="kefu"> <el-container v-if="showChatBox" class="kefu">
<el-header> <el-header>
<!-- TODO @puhui999keFuConversation => conversation -->
<div class="kefu-title">{{ keFuConversation.userNickname }}</div> <div class="kefu-title">{{ keFuConversation.userNickname }}</div>
</el-header> </el-header>
<!-- TODO @puhui999unocss -->
<el-main class="kefu-content" style="overflow: visible"> <el-main class="kefu-content" style="overflow: visible">
<!-- 加载历史消息 -->
<div <div
v-show="loadingMore" v-show="loadingMore"
class="loadingMore flex justify-center items-center cursor-pointer" class="loadingMore flex justify-center items-center cursor-pointer"
@ -13,6 +16,7 @@
</div> </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 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">
<!-- 日期 --> <!-- 日期 -->
@ -118,8 +122,10 @@ import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
defineOptions({ name: 'KeFuMessageBox' }) defineOptions({ name: 'KeFuMessageBox' })
const message = ref('') //
const messageTool = useMessage() const messageTool = useMessage()
const message = ref('') //
const messageList = ref<KeFuMessageRespVO[]>([]) // const messageList = ref<KeFuMessageRespVO[]>([]) //
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
const showNewMessageTip = ref(false) // const showNewMessageTip = ref(false) //
@ -128,7 +134,8 @@ const queryParams = reactive({
conversationId: 0 conversationId: 0
}) })
const total = ref(0) // const total = ref(0) //
//
/** 获得消息列表 */
const getMessageList = async (conversation: KeFuConversationRespVO) => { const getMessageList = async (conversation: KeFuConversationRespVO) => {
keFuConversation.value = conversation keFuConversation.value = conversation
queryParams.conversationId = conversation.id queryParams.conversationId = conversation.id
@ -146,12 +153,14 @@ const getMessageList = async (conversation: KeFuConversationRespVO) => {
} }
await scrollToBottom() await scrollToBottom()
} }
/** 按照时间倒序,获取消息列表 */
const getMessageList0 = computed(() => { const getMessageList0 = computed(() => {
messageList.value.sort((a: any, b: any) => a.createTime - b.createTime) messageList.value.sort((a: any, b: any) => a.createTime - b.createTime)
return messageList.value return messageList.value
}) })
// /** 刷新消息列表 */
const refreshMessageList = async () => { const refreshMessageList = async () => {
if (!keFuConversation.value) { if (!keFuConversation.value) {
return return
@ -164,14 +173,16 @@ const refreshMessageList = async () => {
showNewMessageTip.value = true showNewMessageTip.value = true
} }
} }
defineExpose({ getMessageList, refreshMessageList }) defineExpose({ getMessageList, refreshMessageList })
// const showChatBox = computed(() => !isEmpty(keFuConversation.value)) //
const showChatBox = computed(() => !isEmpty(keFuConversation.value))
// /** 处理表情选择 */
const handleEmojiSelect = (item: Emoji) => { const handleEmojiSelect = (item: Emoji) => {
message.value += item.name message.value += item.name
} }
//
/** 处理图片发送 */
const handleSendPicture = async (picUrl: string) => { const handleSendPicture = async (picUrl: string) => {
// //
const msg = { const msg = {
@ -181,7 +192,8 @@ const handleSendPicture = async (picUrl: string) => {
} }
await sendMessage(msg) await sendMessage(msg)
} }
//
/** 发送文本消息 */
const handleSendMessage = async () => { const handleSendMessage = async () => {
// 1. // 1.
if (isEmpty(unref(message.value))) { if (isEmpty(unref(message.value))) {
@ -197,7 +209,7 @@ const handleSendMessage = async () => {
await sendMessage(msg) await sendMessage(msg)
} }
// /** 真正发送消息 【共用】*/
const sendMessage = async (msg: any) => { const sendMessage = async (msg: any) => {
// //
await KeFuMessageApi.sendKeFuMessage(msg) await KeFuMessageApi.sendKeFuMessage(msg)
@ -208,9 +220,9 @@ const sendMessage = async (msg: any) => {
await scrollToBottom() await scrollToBottom()
} }
/** 滚动到底部 */
const innerRef = ref<HTMLDivElement>() const innerRef = ref<HTMLDivElement>()
const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>() const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
//
const scrollToBottom = async () => { const scrollToBottom = async () => {
// 1. // 1.
if (loadHistory.value) { if (loadHistory.value) {
@ -223,12 +235,14 @@ const scrollToBottom = async () => {
// 2.2 // 2.2
await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id) await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id)
} }
//
/** 查看新消息 */
const handleToNewMessage = async () => { const handleToNewMessage = async () => {
loadHistory.value = false loadHistory.value = false
await scrollToBottom() await scrollToBottom()
} }
/** 加载历史消息 */
const loadingMore = ref(false) // const loadingMore = ref(false) //
const loadHistory = ref(false) // const loadHistory = ref(false) //
const handleScroll = async ({ scrollTop }) => { const handleScroll = async ({ scrollTop }) => {
@ -247,8 +261,10 @@ const handleOldMessage = async () => {
loadingMore.value = false loadingMore.value = false
// TODO puhui999: // TODO puhui999:
} }
/** /**
* 是否显示时间 * 是否显示时间
*
* @param {*} item - 数据 * @param {*} item - 数据
* @param {*} index - 索引 * @param {*} index - 索引
*/ */

View File

@ -1,6 +1,5 @@
<template> <template>
<div class="kefu"> <div class="kefu">
<!-- TODO @puhui999item => conversation 会不会更容易理解 -->
<div <div
v-for="(item, index) in conversationList" v-for="(item, index) in conversationList"
:key="item.id" :key="item.id"

View File

@ -10,6 +10,7 @@
: '' : ''
]" ]"
> >
<!-- TODO @puhui999unocss -->
<el-image <el-image
:src="message.content" :src="message.content"
fit="contain" fit="contain"
@ -30,6 +31,7 @@ defineOptions({ name: 'ImageMessageItem' })
defineProps<{ defineProps<{
message: KeFuMessageRespVO message: KeFuMessageRespVO
}>() }>()
/** 图预览 */ /** 图预览 */
const imagePreview = (imgUrl: string) => { const imagePreview = (imgUrl: string) => {
createImageViewer({ createImageViewer({

View File

@ -18,6 +18,7 @@
</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" :img="item.picUrl"
:num="item.count" :num="item.count"
@ -29,10 +30,10 @@
<div class="pay-box mt-30px flex justify-end pr-20px"> <div class="pay-box mt-30px flex justify-end pr-20px">
<div class="flex items-center"> <div class="flex items-center">
<div class="discounts-title pay-color" <div class="discounts-title pay-color"
> {{ getMessageContent.productCount }} 件商品,总金额: > {{ getMessageContent?.productCount }} 件商品,总金额:
</div> </div>
<div class="discounts-money pay-color"> <div class="discounts-money pay-color">
{{ fenToYuan(getMessageContent.payPrice) }} {{ fenToYuan(getMessageContent?.payPrice) }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -90,6 +90,8 @@ const props = defineProps({
default: '' default: ''
} }
}) })
/** SKU 展示字符串 */
const skuString = computed(() => { const skuString = computed(() => {
if (!props.skuText) { if (!props.skuText) {
return '' return ''
@ -99,6 +101,8 @@ const skuString = computed(() => {
} }
return props.skuText return props.skuText
}) })
// TODO @puhui999使 preview-teleported
/** 图预览 */ /** 图预览 */
const imagePrediv = (imgUrl: string) => { const imagePrediv = (imgUrl: string) => {
createImageViewer({ createImageViewer({

View File

@ -32,5 +32,7 @@ defineOptions({ name: 'ProductMessageItem' })
const props = defineProps<{ const props = defineProps<{
message: KeFuMessageRespVO message: KeFuMessageRespVO
}>() }>()
/** 获悉消息内容 */
const getMessageContent = computed(() => JSON.parse(props.message.content)) const getMessageContent = computed(() => JSON.parse(props.message.content))
</script> </script>

View File

@ -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" 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" style="width: 24px; height: 24px" /> <img :src="item.url" style="width: 24px; height: 24px" />
</li> </li>
</ul> </ul>
@ -31,6 +32,7 @@ import { Emoji, useEmoji } from './emoji'
const { getEmojiList } = useEmoji() const { getEmojiList } = useEmoji()
const emojiList = computed(() => getEmojiList()) const emojiList = computed(() => getEmojiList())
/** 选择 emoji 表情 */
const emits = defineEmits<{ const emits = defineEmits<{
(e: 'select-emoji', v: Emoji) (e: 'select-emoji', v: Emoji)
}>() }>()

View File

@ -1,19 +1,24 @@
<!-- 图片选择 -->
<template> <template>
<div> <div>
<!-- TODO @puhui999unocss -->
<img :src="Picture" style="width: 35px; height: 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/images/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' })
const message = useMessage()
const message = useMessage() //
/** 选择并上传文件 */
const emits = defineEmits<{ const emits = defineEmits<{
(e: 'send-picture', v: string): void (e: 'send-picture', v: string): void
}>() }>()
//
const selectAndUpload = async () => { const selectAndUpload = async () => {
const files: any = await getFiles() const files: any = await getFiles()
message.success('图片发送中请稍等。。。') message.success('图片发送中请稍等。。。')
@ -23,6 +28,7 @@ const selectAndUpload = async () => {
/** /**
* 唤起文件选择窗口并获取选择的文件 * 唤起文件选择窗口并获取选择的文件
*
* @param {Object} options - 配置选项 * @param {Object} options - 配置选项
* @param {boolean} [options.multiple=true] - 是否支持多选 * @param {boolean} [options.multiple=true] - 是否支持多选
* @param {string} [options.accept=''] - 文件上传格式限制 * @param {string} [options.accept=''] - 文件上传格式限制
@ -54,7 +60,7 @@ async function getFiles(options = {}) {
// change // change
try { try {
const files = await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
input.addEventListener('change', (event: any) => { input.addEventListener('change', (event: any) => {
const filesArray = Array.from(event?.target?.files || []) 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) const overSizedFiles = filesArray.filter((file: File) => file.size / 1024 ** 2 > fileSize)
if (oversizedFiles.length > 0) { if (overSizedFiles.length > 0) {
reject({ errorType: 'fileSize', files: oversizedFiles }) reject({ errorType: 'fileSize', files: overSizedFiles })
return return
} }
@ -79,8 +85,6 @@ async function getFiles(options = {}) {
resolve(fileList) resolve(fileList)
}) })
}) })
return files
} catch (error) { } catch (error) {
console.error('选择文件出错:', error) console.error('选择文件出错:', error)
throw error throw error

View File

@ -58,8 +58,11 @@ export interface Emoji {
export const useEmoji = () => { export const useEmoji = () => {
const emojiPathList = ref<any[]>([]) const emojiPathList = ref<any[]>([])
// 加载本地图片
// TODO @puhui999initStaticEmoji 会不会更好
/** 加载本地图片 */
const getStaticEmojiPath = async () => { const getStaticEmojiPath = 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/images/*.{png,jpg,jpeg,svg}'
) )
@ -68,17 +71,25 @@ export const useEmoji = () => {
emojiPathList.value.push(imageModule.default) emojiPathList.value.push(imageModule.default)
} }
} }
// 初始化
/** 初始化 */
onMounted(async () => { onMounted(async () => {
if (isEmpty(emojiPathList.value)) { if (isEmpty(emojiPathList.value)) {
await getStaticEmojiPath() await getStaticEmojiPath()
} }
}) })
// 处理表情 // TODO @puhui999建议 function 都改成 const 这种来定义哈。保持统一风格
/**
*
*
* @param data TODO @puhui999data => content
* @return
*/
function replaceEmoji(data: string) { function replaceEmoji(data: string) {
let newData = data let newData = data
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) {
@ -94,7 +105,11 @@ export const useEmoji = () => {
return newData return newData
} }
// 获得所有表情 /**
*
*
* @return
*/
function getEmojiList(): Emoji[] { function getEmojiList(): Emoji[] {
return emojiList.map((item) => ({ return emojiList.map((item) => ({
url: selEmojiFile(item.name), url: selEmojiFile(item.name),
@ -102,6 +117,7 @@ export const useEmoji = () => {
})) as Emoji[] })) as Emoji[]
} }
// TODO @puhui999getEmojiFileByName 会不会更容易理解哈
function selEmojiFile(name: string) { function selEmojiFile(name: string) {
for (const emoji of emojiList) { for (const emoji of emojiList) {
if (emoji.name === name) { if (emoji.name === name) {