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/KeFuConversationList.vue |  254 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 254 insertions(+), 0 deletions(-)

diff --git a/src/views/mall/promotion/kefu/components/KeFuConversationList.vue b/src/views/mall/promotion/kefu/components/KeFuConversationList.vue
new file mode 100644
index 0000000..318e27d
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/KeFuConversationList.vue
@@ -0,0 +1,254 @@
+<template>
+  <el-aside class="kefu pt-5px h-100%" width="260px">
+    <div class="color-[#999] font-bold my-10px">
+      浼氳瘽璁板綍({{ kefuStore.getConversationList.length }})
+    </div>
+    <div
+      v-for="item in kefuStore.getConversationList"
+      :key="item.id"
+      :class="{ active: item.id === activeConversationId, pinned: item.adminPinned }"
+      class="kefu-conversation px-10px flex items-center"
+      @click="openRightMessage(item)"
+      @contextmenu.prevent="rightClick($event as PointerEvent, item)"
+    >
+      <div class="flex justify-center items-center w-100%">
+        <div class="flex justify-center items-center w-50px h-50px">
+          <!-- 澶村儚 + 鏈 -->
+          <el-badge
+            :hidden="item.adminUnreadMessageCount === 0"
+            :max="99"
+            :value="item.adminUnreadMessageCount"
+          >
+            <el-avatar :src="item.userAvatar" alt="avatar" />
+          </el-badge>
+        </div>
+        <div class="ml-10px w-100%">
+          <div class="flex justify-between items-center w-100%">
+            <span class="username">{{ item.userNickname }}</span>
+            <span class="color-[#999]" style="font-size: 13px">
+              {{ lastMessageTimeMap.get(item.id) ?? '璁$畻涓�' }}
+            </span>
+          </div>
+          <!-- 鏈�鍚庤亰澶╁唴瀹� -->
+          <div
+            v-dompurify-html="
+              getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
+            "
+            class="last-message flex items-center color-[#999]"
+          >
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 鍙抽敭锛岃繘琛屾搷浣滐紙绫讳技寰俊锛� -->
+    <ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul">
+      <li
+        v-show="!rightClickConversation.adminPinned"
+        class="flex items-center"
+        @click.stop="updateConversationPinned(true)"
+      >
+        <Icon class="mr-5px" icon="ep:top" />
+        缃《浼氳瘽
+      </li>
+      <li
+        v-show="rightClickConversation.adminPinned"
+        class="flex items-center"
+        @click.stop="updateConversationPinned(false)"
+      >
+        <Icon class="mr-5px" icon="ep:bottom" />
+        鍙栨秷缃《
+      </li>
+      <li class="flex items-center" @click.stop="deleteConversation">
+        <Icon class="mr-5px" color="red" icon="ep:delete" />
+        鍒犻櫎浼氳瘽
+      </li>
+      <li class="flex items-center" @click.stop="closeRightMenu">
+        <Icon class="mr-5px" color="red" icon="ep:close" />
+        鍙栨秷
+      </li>
+    </ul>
+  </el-aside>
+</template>
+
+<script lang="ts" setup>
+import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { useEmoji } from './tools/emoji'
+import { formatPast } from '@/utils/formatTime'
+import { KeFuMessageContentTypeEnum } from './tools/constants'
+import { useAppStore } from '@/store/modules/app'
+import { useMallKefuStore } from '@/store/modules/mall/kefu'
+import { jsonParse } from '@/utils'
+
+defineOptions({ name: 'KeFuConversationList' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const appStore = useAppStore()
+const kefuStore = useMallKefuStore() // 瀹㈡湇缂撳瓨
+const { replaceEmoji } = useEmoji()
+const activeConversationId = ref(-1) // 閫変腑鐨勪細璇�
+const collapse = computed(() => appStore.getCollapse) // 鎶樺彔鑿滃崟
+
+/** 璁$畻娑堟伅鏈�鍚庡彂閫佹椂闂磋窛绂荤幇鍦ㄨ繃鍘讳簡澶氫箙 */
+const lastMessageTimeMap = ref<Map<number, string>>(new Map<number, string>())
+const calculationLastMessageTime = () => {
+  kefuStore.getConversationList?.forEach((item) => {
+    lastMessageTimeMap.value.set(item.id, formatPast(item.lastMessageTime, 'YYYY-MM-DD'))
+  })
+}
+defineExpose({ calculationLastMessageTime })
+
+/** 鎵撳紑鍙充晶鐨勬秷鎭垪琛� */
+const emits = defineEmits<{
+  (e: 'change', v: KeFuConversationRespVO): void
+}>()
+const openRightMessage = (item: KeFuConversationRespVO) => {
+  // 鍚屼竴涓細璇濆垯涓嶅鐞�
+  if (activeConversationId.value === item.id) {
+    return
+  }
+  activeConversationId.value = item.id
+  emits('change', item)
+}
+
+/** 鑾峰緱娑堟伅绫诲瀷 */
+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(jsonParse(lastMessageContent).text || lastMessageContent)
+      default:
+        return ''
+    }
+  }
+)
+
+//======================= 鍙抽敭鑿滃崟 =======================
+const showRightMenu = ref(false) // 鏄剧ず鍙抽敭鑿滃崟
+const rightMenuStyle = ref<any>({}) // 鍙抽敭鑿滃崟 Style
+const rightClickConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 鍙抽敭閫変腑鐨勪細璇濆璞�
+
+/** 鎵撳紑鍙抽敭鑿滃崟 */
+const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
+  rightClickConversation.value = item
+  // 鏄剧ず鍙抽敭鑿滃崟
+  showRightMenu.value = true
+  rightMenuStyle.value = {
+    top: mouseEvent.clientY - 110 + 'px',
+    left: collapse.value ? mouseEvent.clientX - 80 + 'px' : mouseEvent.clientX - 210 + 'px'
+  }
+}
+/** 鍏抽棴鍙抽敭鑿滃崟 */
+const closeRightMenu = () => {
+  showRightMenu.value = false
+}
+
+/** 缃《浼氳瘽 */
+const updateConversationPinned = async (adminPinned: boolean) => {
+  // 1. 浼氳瘽缃《/鍙栨秷缃《
+  await KeFuConversationApi.updateConversationPinned({
+    id: rightClickConversation.value.id,
+    adminPinned
+  })
+  message.notifySuccess(adminPinned ? '缃《鎴愬姛' : '鍙栨秷缃《鎴愬姛')
+  // 2. 鍏抽棴鍙抽敭鑿滃崟锛屾洿鏂颁細璇濆垪琛�
+  closeRightMenu()
+  await kefuStore.updateConversation(rightClickConversation.value.id)
+}
+
+/** 鍒犻櫎浼氳瘽 */
+const deleteConversation = async () => {
+  // 1. 鍒犻櫎浼氳瘽
+  await message.confirm('鎮ㄧ‘瀹氳鍒犻櫎璇ヤ細璇濆悧锛�')
+  await KeFuConversationApi.deleteConversation(rightClickConversation.value.id)
+  // 2. 鍏抽棴鍙抽敭鑿滃崟锛屾洿鏂颁細璇濆垪琛�
+  closeRightMenu()
+  kefuStore.deleteConversation(rightClickConversation.value.id)
+}
+
+/** 鐩戝惉鍙抽敭鑿滃崟鐨勬樉绀虹姸鎬侊紝娣诲姞鐐瑰嚮浜嬩欢鐩戝惉鍣� */
+watch(showRightMenu, (val) => {
+  if (val) {
+    document.body.addEventListener('click', closeRightMenu)
+  } else {
+    document.body.removeEventListener('click', closeRightMenu)
+  }
+})
+
+const timer = ref<any>()
+/** 鍒濆鍖� */
+onMounted(() => {
+  timer.value = setInterval(calculationLastMessageTime, 1000 * 10) // 鍗佺璁$畻涓�娆�
+})
+/** 缁勪欢鍗歌浇鍓� */
+onBeforeUnmount(() => {
+  clearInterval(timer.value)
+})
+</script>
+
+<style lang="scss" scoped>
+.kefu {
+  background-color: var(--app-content-bg-color);
+
+  &-conversation {
+    height: 60px;
+    //background-color: #fff;
+    //transition: border-left 0.05s ease-in-out; /* 璁剧疆杩囨浮鏁堟灉 */
+
+    .username {
+      min-width: 0;
+      max-width: 60%;
+    }
+
+    .last-message {
+      font-size: 13px;
+    }
+
+    .last-message,
+    .username {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      display: -webkit-box;
+      -webkit-box-orient: vertical;
+      -webkit-line-clamp: 1;
+    }
+  }
+
+  .active {
+    background-color: rgba(128, 128, 128, 0.5); // 閫忔槑鑹诧紝鏆楅粦妯″紡涓嬩篃鑳戒綋鐜�
+  }
+
+  .right-menu-ul {
+    position: absolute;
+    background-color: var(--app-content-bg-color);
+    padding: 5px;
+    margin: 0;
+    list-style-type: none; /* 绉婚櫎榛樿鐨勯」鐩鍙� */
+    border-radius: 12px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 闃村奖鏁堟灉 */
+    width: 130px;
+
+    li {
+      padding: 8px 16px;
+      cursor: pointer;
+      border-radius: 12px;
+      transition: background-color 0.3s; /* 骞虫粦杩囨浮 */
+      &:hover {
+        background-color: var(--left-menu-bg-active-color); /* 鎮仠鏃剁殑鑳屾櫙棰滆壊 */
+      }
+    }
+  }
+}
+</style>

--
Gitblit v1.8.0