From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/views/mall/promotion/kefu/components/KeFuMessageList.vue |  526 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 526 insertions(+), 0 deletions(-)

diff --git a/src/views/mall/promotion/kefu/components/KeFuMessageList.vue b/src/views/mall/promotion/kefu/components/KeFuMessageList.vue
new file mode 100644
index 0000000..a293735
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/KeFuMessageList.vue
@@ -0,0 +1,526 @@
+<template>
+  <el-container v-if="showKeFuMessageList" class="kefu">
+    <el-header class="kefu-header">
+      <div class="kefu-title">{{ conversation.userNickname }}</div>
+    </el-header>
+    <el-main class="kefu-content overflow-visible">
+      <el-scrollbar ref="scrollbarRef" always @scroll="handleScroll">
+        <div v-if="refreshContent" ref="innerRef" class="w-[100%] px-10px">
+          <!-- 娑堟伅鍒楄〃 -->
+          <div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
+            <div class="flex justify-center items-center mb-20px">
+              <!-- 鏃ユ湡 -->
+              <div
+                v-if="
+                  item.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item, index)
+                "
+                class="date-message"
+              >
+                {{ formatDate(item.createTime) }}
+              </div>
+              <!-- 绯荤粺娑堟伅 -->
+              <div
+                v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM"
+                class="system-message"
+              >
+                {{ item.content }}
+              </div>
+            </div>
+            <div
+              :class="[
+                item.senderType === UserTypeEnum.MEMBER
+                  ? `ss-row-left`
+                  : item.senderType === UserTypeEnum.ADMIN
+                    ? `ss-row-right`
+                    : ''
+              ]"
+              class="flex mb-20px w-[100%]"
+            >
+              <el-avatar
+                v-if="item.senderType === UserTypeEnum.MEMBER"
+                :src="conversation.userAvatar"
+                alt="avatar"
+                class="w-60px h-60px"
+              />
+              <div
+                :class="{
+                  'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType
+                }"
+              >
+                <!-- 鏂囨湰娑堟伅 -->
+                <MessageItem :message="item">
+                  <template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
+                    <div
+                      v-dompurify-html="replaceEmoji(getMessageContent(item).text || item.content)"
+                      class="line-height-normal text-justify h-1/1 w-full"
+                    ></div>
+                  </template>
+                </MessageItem>
+                <!-- 鍥剧墖娑堟伅 -->
+                <MessageItem :message="item">
+                  <el-image
+                    v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
+                    :initial-index="0"
+                    :preview-src-list="[getMessageContent(item).picUrl || item.content]"
+                    :src="getMessageContent(item).picUrl || item.content"
+                    class="w-200px mx-10px"
+                    fit="contain"
+                    preview-teleported
+                  />
+                </MessageItem>
+                <!-- 鍟嗗搧娑堟伅 -->
+                <MessageItem :message="item">
+                  <ProductItem
+                    v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
+                    :picUrl="getMessageContent(item).picUrl"
+                    :price="getMessageContent(item).price"
+                    :sales-count="getMessageContent(item).salesCount"
+                    :spuId="getMessageContent(item).spuId"
+                    :stock="getMessageContent(item).stock"
+                    :title="getMessageContent(item).spuName"
+                    class="max-w-300px mx-10px"
+                  />
+                </MessageItem>
+                <!-- 璁㈠崟娑堟伅 -->
+                <MessageItem :message="item">
+                  <OrderItem
+                    v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
+                    :message="item"
+                    class="max-w-100% mx-10px"
+                  />
+                </MessageItem>
+              </div>
+              <el-avatar
+                v-if="item.senderType === UserTypeEnum.ADMIN"
+                :src="item.senderAvatar"
+                alt="avatar"
+              />
+            </div>
+          </div>
+        </div>
+      </el-scrollbar>
+      <div
+        v-show="showNewMessageTip"
+        class="newMessageTip flex items-center cursor-pointer"
+        @click="handleToNewMessage"
+      >
+        <span>鏈夋柊娑堟伅</span>
+        <Icon class="ml-5px" icon="ep:bottom" />
+      </div>
+    </el-main>
+    <el-footer class="kefu-footer">
+      <div class="chat-tools flex items-center">
+        <EmojiSelectPopover @select-emoji="handleEmojiSelect" />
+        <PictureSelectUpload
+          class="ml-15px mt-3px cursor-pointer"
+          @send-picture="handleSendPicture"
+        />
+      </div>
+      <el-input
+        v-model="message"
+        :rows="6"
+        placeholder="杈撳叆娑堟伅锛孍nter鍙戦�侊紝Shift+Enter鎹㈣"
+        style="border-style: none"
+        type="textarea"
+        @keyup.enter.prevent="handleSendMessage"
+      />
+    </el-footer>
+  </el-container>
+  <el-container v-else class="kefu">
+    <el-main>
+      <el-empty description="璇烽�夋嫨宸︿晶鐨勪竴涓細璇濆悗寮�濮�" />
+    </el-main>
+  </el-container>
+</template>
+
+<script lang="ts" setup>
+import { ElScrollbar as ElScrollbarType } from 'element-plus'
+import { KeFuMessageApi, KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
+import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import EmojiSelectPopover from './tools/EmojiSelectPopover.vue'
+import PictureSelectUpload from './tools/PictureSelectUpload.vue'
+import ProductItem from './message/ProductItem.vue'
+import OrderItem from './message/OrderItem.vue'
+import { Emoji, useEmoji } from './tools/emoji'
+import { KeFuMessageContentTypeEnum } from './tools/constants'
+import { isEmpty } from '@/utils/is'
+import { UserTypeEnum } from '@/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import { debounce } from 'lodash-es'
+import { jsonParse } from '@/utils'
+import { useMallKefuStore } from '@/store/modules/mall/kefu'
+
+dayjs.extend(relativeTime)
+
+defineOptions({ name: 'KeFuMessageList' })
+
+const message = ref('') // 娑堟伅寮圭獥
+const { replaceEmoji } = useEmoji()
+const messageTool = useMessage()
+const messageList = ref<KeFuMessageRespVO[]>([]) // 娑堟伅鍒楄〃
+const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 鐢ㄦ埛浼氳瘽
+const showNewMessageTip = ref(false) // 鏄剧ず鏈夋柊娑堟伅鎻愮ず
+const queryParams = reactive({
+  conversationId: 0,
+  createTime: undefined
+})
+const total = ref(0) // 娑堟伅鎬绘潯鏁�
+const refreshContent = ref(false) // 鍐呭鍒锋柊,涓昏瑙e喅浼氳瘽娑堟伅椤甸潰楂樺害涓嶄竴鑷村鑷寸殑婊氬姩鍔熻兘绮惧害澶辨晥
+const kefuStore = useMallKefuStore() // 瀹㈡湇缂撳瓨
+
+/** 鑾锋倝娑堟伅鍐呭 */
+const getMessageContent = computed(() => (item: any) => jsonParse(item.content))
+/** 鑾峰緱娑堟伅鍒楄〃 */
+const getMessageList = async () => {
+  const res = await KeFuMessageApi.getKeFuMessageList(queryParams)
+  if (isEmpty(res)) {
+    // 褰撹繑鍥炵殑鏄┖鍒楄〃璇存槑娌℃湁娑堟伅鎴栬�呭凡缁忔煡璇㈠畬浜嗗巻鍙叉秷鎭�
+    skipGetMessageList.value = true
+    return
+  }
+  queryParams.createTime = formatDate(res.at(-1).createTime) as any
+
+  // 鎯呭喌涓�锛氬姞杞芥渶鏂版秷鎭�
+  if (!queryParams.createTime) {
+    messageList.value = res
+  } else {
+    // 鎯呭喌浜岋細鍔犺浇鍘嗗彶娑堟伅
+    for (const item of res) {
+      pushMessage(item)
+    }
+  }
+  refreshContent.value = true
+}
+
+/** 娣诲姞娑堟伅 */
+const pushMessage = (message: any) => {
+  if (messageList.value.some((val) => val.id === message.id)) {
+    return
+  }
+  messageList.value.push(message)
+}
+
+/** 鎸夌収鏃堕棿鍊掑簭锛岃幏鍙栨秷鎭垪琛� */
+const getMessageList0 = computed(() => {
+  messageList.value.sort((a: any, b: any) => a.createTime - b.createTime)
+  return messageList.value
+})
+
+/** 鍒锋柊娑堟伅鍒楄〃 */
+const refreshMessageList = async (message?: any) => {
+  if (!conversation.value) {
+    return
+  }
+
+  if (typeof message !== 'undefined') {
+    // 褰撳墠鏌ヨ浼氳瘽涓庢秷鎭墍灞炰細璇濅笉涓�鑷村垯涓嶅仛澶勭悊
+    if (message.conversationId !== conversation.value.id) {
+      return
+    }
+    pushMessage(message)
+  } else {
+    queryParams.createTime = undefined
+    await getMessageList()
+  }
+
+  if (loadHistory.value) {
+    // 鍙充笅瑙掓樉绀烘湁鏂版秷鎭彁绀�
+    showNewMessageTip.value = true
+  } else {
+    // 婊氬姩鍒版渶鏂版秷鎭
+    await handleToNewMessage()
+  }
+}
+
+/** 鑾峰緱鏂颁細璇濈殑娑堟伅鍒楄〃, 鐐瑰嚮鍒囨崲鏃讹紝璇诲彇缂撳瓨锛涚劧鍚庡紓姝ヨ幏鍙栨柊娑堟伅锛宮erge 涓嬶紱 */
+const getNewMessageList = async (val: KeFuConversationRespVO) => {
+  // 1. 缂撳瓨褰撳墠浼氳瘽娑堟伅鍒楄〃
+  kefuStore.saveMessageList(conversation.value.id, messageList.value)
+  // 2.1 浼氳瘽鍒囨崲,閲嶇疆鐩稿叧鍙傛暟
+  messageList.value = kefuStore.getConversationMessageList(val.id) || []
+  total.value = messageList.value.length || 0
+  loadHistory.value = false
+  refreshContent.value = false
+  skipGetMessageList.value = false
+  // 2.2 璁剧疆浼氳瘽鐩稿叧灞炴��
+  conversation.value = val
+  queryParams.conversationId = val.id
+  queryParams.createTime = undefined
+  // 3. 鑾峰彇娑堟伅
+  await refreshMessageList()
+}
+defineExpose({ getNewMessageList, refreshMessageList })
+
+const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 鏄惁鏄剧ず鑱婂ぉ鍖哄煙
+const skipGetMessageList = ref(false) // 璺宠繃娑堟伅鑾峰彇
+
+/** 澶勭悊琛ㄦ儏閫夋嫨 */
+const handleEmojiSelect = (item: Emoji) => {
+  message.value += item.name
+}
+
+/** 澶勭悊鍥剧墖鍙戦�� */
+const handleSendPicture = async (picUrl: string) => {
+  // 缁勭粐鍙戦�佹秷鎭�
+  const msg = {
+    conversationId: conversation.value.id,
+    contentType: KeFuMessageContentTypeEnum.IMAGE,
+    content: JSON.stringify({ picUrl })
+  }
+  await sendMessage(msg)
+}
+
+/** 鍙戦�佹枃鏈秷鎭� */
+const handleSendMessage = async (event: any) => {
+  // shift 涓嶅彂閫�
+  if (event.shiftKey) {
+    return
+  }
+  // 1. 鏍¢獙娑堟伅鏄惁涓虹┖
+  if (isEmpty(unref(message.value)?.trim())) {
+    messageTool.notifyWarning('璇疯緭鍏ユ秷鎭悗鍐嶅彂閫佸摝锛�')
+    message.value = ''
+    return
+  }
+  // 2. 缁勭粐鍙戦�佹秷鎭�
+  const msg = {
+    conversationId: conversation.value.id,
+    contentType: KeFuMessageContentTypeEnum.TEXT,
+    content: JSON.stringify({ text: message.value })
+  }
+  await sendMessage(msg)
+}
+
+/** 鐪熸鍙戦�佹秷鎭� 銆愬叡鐢ㄣ��*/
+const sendMessage = async (msg: any) => {
+  // 鍙戦�佹秷鎭�
+  await KeFuMessageApi.sendKeFuMessage(msg)
+  message.value = ''
+  // 鍔犺浇娑堟伅鍒楄〃
+  await refreshMessageList()
+  // 鏇存柊浼氳瘽缂撳瓨
+  await kefuStore.updateConversation(conversation.value.id)
+}
+
+/** 婊氬姩鍒板簳閮� */
+const innerRef = ref<HTMLDivElement>()
+const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
+const scrollToBottom = async () => {
+  // 1. 棣栨鍔犺浇鏃舵粴鍔ㄥ埌鏈�鏂版秷鎭紝濡傛灉鍔犺浇鐨勬槸鍘嗗彶娑堟伅鍒欎笉婊氬姩
+  if (loadHistory.value) {
+    return
+  }
+  // 2.1 婊氬姩鍒版渶鏂版秷鎭紝鍏抽棴鏂版秷鎭彁绀�
+  await nextTick()
+  scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
+  showNewMessageTip.value = false
+  // 2.2 娑堟伅宸茶
+  await KeFuMessageApi.updateKeFuMessageReadStatus(conversation.value.id)
+}
+
+/** 鏌ョ湅鏂版秷鎭� */
+const handleToNewMessage = async () => {
+  loadHistory.value = false
+  await scrollToBottom()
+}
+
+const loadHistory = ref(false) // 鍔犺浇鍘嗗彶娑堟伅
+/** 澶勭悊娑堟伅鍒楄〃婊氬姩浜嬩欢(debounce 闄愭祦) */
+const handleScroll = debounce(({ scrollTop }) => {
+  if (skipGetMessageList.value) {
+    return
+  }
+  // 瑙﹂《鑷姩鍔犺浇涓嬩竴椤垫暟鎹�
+  if (Math.floor(scrollTop) === 0) {
+    handleOldMessage()
+  }
+  const wrap = scrollbarRef.value?.wrapRef
+  // 瑙﹀簳閲嶇疆
+  if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
+    loadHistory.value = false
+    refreshMessageList()
+  }
+}, 200)
+/** 鍔犺浇鍘嗗彶娑堟伅 */
+const handleOldMessage = async () => {
+  // 璁板綍宸叉湁椤甸潰楂樺害
+  const oldPageHeight = innerRef.value?.clientHeight
+  if (!oldPageHeight) {
+    return
+  }
+  loadHistory.value = true
+  await getMessageList()
+  // 绛夐〉闈㈠姞杞藉畬鍚庯紝鑾峰緱涓婁竴椤垫渶鍚庝竴鏉℃秷鎭殑浣嶇疆锛屾帶鍒舵粴鍔ㄥ埌瀹冩墍鍦ㄤ綅缃�
+  scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
+}
+
+/**
+ * 鏄惁鏄剧ず鏃堕棿
+ *
+ * @param {*} item - 鏁版嵁
+ * @param {*} index - 绱㈠紩
+ */
+const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
+  if (unref(messageList.value)[index + 1]) {
+    let dateString = dayjs(unref(messageList.value)[index + 1].createTime).fromNow()
+    return dateString !== dayjs(unref(item).createTime).fromNow()
+  }
+  return false
+})
+</script>
+
+<style lang="scss" scoped>
+.kefu {
+  background-color: var(--app-content-bg-color);
+  position: relative;
+  width: calc(100% - 300px - 260px);
+
+  &::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 1px; /* 瀹為檯瀹藉害 */
+    height: 100%;
+    background-color: var(--el-border-color);
+    transform: scaleX(0.3); /* 缂╁皬瀹藉害 */
+  }
+
+  .kefu-header {
+    background-color: var(--app-content-bg-color);
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    &::before {
+      content: '';
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      width: 100%;
+      height: 1px; /* 鍒濆瀹藉害 */
+      background-color: var(--el-border-color);
+      transform: scaleY(0.3); /* 缂╁皬瑙嗚楂樺害 */
+    }
+
+    &-title {
+      font-size: 18px;
+      font-weight: bold;
+    }
+  }
+
+  &-content {
+    margin: 0;
+    padding: 10px;
+    position: relative;
+    height: 100%;
+    width: 100%;
+
+    .newMessageTip {
+      position: absolute;
+      bottom: 35px;
+      right: 35px;
+      background-color: var(--app-content-bg-color);
+      padding: 10px;
+      border-radius: 30px;
+      font-size: 12px;
+      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 闃村奖鏁堟灉 */
+    }
+
+    .ss-row-left {
+      justify-content: flex-start;
+
+      .kefu-message {
+        background-color: #fff;
+        margin-left: 10px;
+        margin-top: 3px;
+        border-top-right-radius: 10px;
+        border-bottom-right-radius: 10px;
+        border-bottom-left-radius: 10px;
+      }
+    }
+
+    .ss-row-right {
+      justify-content: flex-end;
+
+      .kefu-message {
+        background-color: rgb(206, 223, 255);
+        margin-right: 10px;
+        margin-top: 3px;
+        border-top-left-radius: 10px;
+        border-bottom-right-radius: 10px;
+        border-bottom-left-radius: 10px;
+      }
+    }
+
+    // 娑堟伅姘旀场
+    .kefu-message {
+      color: #414141;
+      font-weight: 500;
+      padding: 5px 10px;
+      width: auto;
+      max-width: 50%;
+      //text-align: left;
+      //display: inline-block !important;
+      //word-break: break-all;
+      transition: all 0.2s;
+
+      &:hover {
+        transform: scale(1.03);
+      }
+    }
+
+    .date-message,
+    .system-message {
+      width: fit-content;
+      background-color: rgba(0, 0, 0, 0.1);
+      border-radius: 8px;
+      padding: 0 5px;
+      color: #fff;
+      font-size: 10px;
+    }
+  }
+
+  .kefu-footer {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    height: auto;
+    margin: 0;
+    padding: 0;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 1px; /* 鍒濆瀹藉害 */
+      background-color: var(--el-border-color);
+      transform: scaleY(0.3); /* 缂╁皬瑙嗚楂樺害 */
+    }
+
+    .chat-tools {
+      width: 100%;
+      height: 44px;
+    }
+  }
+
+  ::v-deep(textarea) {
+    resize: none;
+    background-color: var(--app-content-bg-color);
+  }
+
+  :deep(.el-input__wrapper) {
+    box-shadow: none !important;
+    border-radius: 0;
+  }
+
+  ::v-deep(.el-textarea__inner) {
+    box-shadow: none !important;
+  }
+}
+</style>

--
Gitblit v1.8.0