【优化】根据代码评审优化 mall 客服
@@ -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)"
 | 
			
		||||
            v-dompurify-html="
 | 
			
		||||
              getConversationDisplayText(item.lastMessageContentType, 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>
 | 
			
		||||
      </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)"
 | 
			
		||||
      >
 | 
			
		||||
@@ -86,7 +79,7 @@ const message = useMessage() // 消息弹窗
 | 
			
		||||
 | 
			
		||||
const { replaceEmoji } = useEmoji()
 | 
			
		||||
const conversationList = ref<KeFuConversationRespVO[]>([]) // 会话列表
 | 
			
		||||
const activeConversationIndex = ref(-1) // 选中的会话 index 位置 TODO @puhui999:这个可以改成 activeConversationId 么?因为一般是选中的对话编号
 | 
			
		||||
const activeConversationId = ref(-1) // 选中的会话
 | 
			
		||||
 | 
			
		||||
/** 加载会话列表 */
 | 
			
		||||
const getConversationList = async () => {
 | 
			
		||||
@@ -98,14 +91,14 @@ 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) => {
 | 
			
		||||
const getConversationDisplayText = computed(
 | 
			
		||||
  () => (lastMessageContentType: number, lastMessageContent: string) => {
 | 
			
		||||
    switch (lastMessageContentType) {
 | 
			
		||||
      case KeFuMessageContentTypeEnum.SYSTEM:
 | 
			
		||||
        return '[系统消息]'
 | 
			
		||||
@@ -119,19 +112,22 @@ const getContentType = computed(() => (lastMessageContentType: number) => {
 | 
			
		||||
        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 = {
 | 
			
		||||
@@ -148,7 +144,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 +157,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,11 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-container v-if="showChatBox" 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">
 | 
			
		||||
    <el-main class="kefu-content overflow-visible">
 | 
			
		||||
      <!-- 加载历史消息 -->
 | 
			
		||||
      <div
 | 
			
		||||
        v-show="loadingMore"
 | 
			
		||||
@@ -48,7 +46,7 @@
 | 
			
		||||
            >
 | 
			
		||||
              <el-avatar
 | 
			
		||||
                v-if="item.senderType === UserTypeEnum.MEMBER"
 | 
			
		||||
                :src="keFuConversation.userAvatar"
 | 
			
		||||
                :src="conversation.userAvatar"
 | 
			
		||||
                alt="avatar"
 | 
			
		||||
              />
 | 
			
		||||
              <div
 | 
			
		||||
@@ -127,7 +125,7 @@ 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,
 | 
			
		||||
@@ -136,9 +134,9 @@ const queryParams = reactive({
 | 
			
		||||
const total = ref(0) // 消息总条数
 | 
			
		||||
 | 
			
		||||
/** 获得消息列表 */
 | 
			
		||||
const getMessageList = async (conversation: KeFuConversationRespVO) => {
 | 
			
		||||
  keFuConversation.value = conversation
 | 
			
		||||
  queryParams.conversationId = conversation.id
 | 
			
		||||
const getMessageList = async (val: KeFuConversationRespVO) => {
 | 
			
		||||
  conversation.value = val
 | 
			
		||||
  queryParams.conversationId = val.id
 | 
			
		||||
  const messageTotal = messageList.value.length
 | 
			
		||||
  if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
 | 
			
		||||
    return
 | 
			
		||||
@@ -162,12 +160,12 @@ const getMessageList0 = computed(() => {
 | 
			
		||||
 | 
			
		||||
/** 刷新消息列表 */
 | 
			
		||||
const refreshMessageList = async () => {
 | 
			
		||||
  if (!keFuConversation.value) {
 | 
			
		||||
  if (!conversation.value) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  queryParams.pageNo = 1
 | 
			
		||||
  await getMessageList(keFuConversation.value)
 | 
			
		||||
  await getMessageList(conversation.value)
 | 
			
		||||
  if (loadHistory.value) {
 | 
			
		||||
    // 有下角显示有新消息提示
 | 
			
		||||
    showNewMessageTip.value = true
 | 
			
		||||
@@ -175,7 +173,7 @@ const refreshMessageList = async () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({ getMessageList, refreshMessageList })
 | 
			
		||||
const showChatBox = computed(() => !isEmpty(keFuConversation.value)) // 是否显示聊天区域
 | 
			
		||||
const showChatBox = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
 | 
			
		||||
 | 
			
		||||
/** 处理表情选择 */
 | 
			
		||||
const handleEmojiSelect = (item: Emoji) => {
 | 
			
		||||
@@ -186,7 +184,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 +200,7 @@ const handleSendMessage = async () => {
 | 
			
		||||
  }
 | 
			
		||||
  // 2. 组织发送消息
 | 
			
		||||
  const msg = {
 | 
			
		||||
    conversationId: keFuConversation.value.id,
 | 
			
		||||
    conversationId: conversation.value.id,
 | 
			
		||||
    contentType: KeFuMessageContentTypeEnum.TEXT,
 | 
			
		||||
    content: message.value
 | 
			
		||||
  }
 | 
			
		||||
@@ -215,7 +213,7 @@ const sendMessage = async (msg: any) => {
 | 
			
		||||
  await KeFuMessageApi.sendKeFuMessage(msg)
 | 
			
		||||
  message.value = ''
 | 
			
		||||
  // 加载消息列表
 | 
			
		||||
  await getMessageList(keFuConversation.value)
 | 
			
		||||
  await getMessageList(conversation.value)
 | 
			
		||||
  // 滚动到最新消息处
 | 
			
		||||
  await scrollToBottom()
 | 
			
		||||
}
 | 
			
		||||
@@ -233,7 +231,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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 查看新消息 */
 | 
			
		||||
@@ -257,7 +255,7 @@ const handleOldMessage = async () => {
 | 
			
		||||
  loadHistory.value = true
 | 
			
		||||
  // 加载消息列表
 | 
			
		||||
  queryParams.pageNo += 1
 | 
			
		||||
  await getMessageList(keFuConversation.value)
 | 
			
		||||
  await getMessageList(conversation.value)
 | 
			
		||||
  loadingMore.value = false
 | 
			
		||||
  // TODO puhui999: 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
 | 
			
		||||
}
 | 
			
		||||
| 
		 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
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@@ -78,13 +77,13 @@ 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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||