From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块
---
src/views/im/home/pages/conversation/components/conversation/conversation-item.vue | 330 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 330 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/pages/conversation/components/conversation/conversation-item.vue b/src/views/im/home/pages/conversation/components/conversation/conversation-item.vue
new file mode 100644
index 0000000..c7b1c51
--- /dev/null
+++ b/src/views/im/home/pages/conversation/components/conversation/conversation-item.vue
@@ -0,0 +1,330 @@
+<script lang="ts" setup>
+import type { Conversation } from '../../../../types'
+
+import { computed } from 'vue'
+
+import { confirm } from '#/packages/effects/common-ui/src'
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { Tag } from 'ant-design-vue'
+
+import { buildRecallTip } from '#/views/im/utils/conversation'
+import { formatConversationTime } from '#/views/im/utils/time'
+import { getSenderDisplayName } from '#/views/im/utils/user'
+
+import { ImContentType, ImConversationType, isNormalMessage } from '../../../../../utils/constants'
+import { GroupAvatar } from '../../../../components/group'
+import { UserAvatar } from '../../../../components/user'
+import { useConversationStore } from '../../../../store/conversationStore'
+import { useFriendStore } from '../../../../store/friendStore'
+import { useGroupRequestStore } from '../../../../store/groupRequestStore'
+import { useGroupStore } from '../../../../store/groupStore'
+import { useImUiStore } from '../../../../store/uiStore'
+
+defineOptions({ name: 'ImConversationItem' })
+
+// 鍛ㄤ腑鏂囧悕锛坉ayjs 鐨� day() 杩斿洖 0-6锛�0=鍛ㄦ棩锛夛紱椤圭洰娌″叏灞�瑁� dayjs/locale/zh-cn锛屾湰鍦版槧灏勯伩鍏嶅紩鍓綔鐢�
+const props = defineProps<{
+ conversation: Conversation
+}>()
+
+const conversationStore = useConversationStore()
+const friendStore = useFriendStore()
+const groupStore = useGroupStore()
+const groupRequestStore = useGroupRequestStore()
+const uiStore = useImUiStore()
+
+const isActive = computed(
+ () =>
+ conversationStore.activeConversation?.targetId === props.conversation.targetId &&
+ conversationStore.activeConversation?.type === props.conversation.type
+)
+
+/**
+ * 褰撳墠浼氳瘽鐨勮崏绋垮揩鐓э細瀛樺湪鏃跺垪琛ㄦ樉绀� [鑽夌] 鍓嶇紑 + 鏂囨湰锛岀洊鎺� sender 鍓嶇紑鍜� @鎴� 绾㈠瓧
+ * 褰撳墠鎵撳紑鐨勪細璇濅笉灞曠ず鑽夌锛涜緭鍏ュ唴瀹瑰凡缁忓湪缂栬緫鍣ㄩ噷鍙锛屽垪琛ㄥ啀鏄剧ず [鑽夌] 鍙嶈�屽啑浣�
+ */
+const draft = computed(() => {
+ if (isActive.value) {
+ return undefined
+ }
+ return conversationStore.getConversationDraft(props.conversation)
+})
+
+const isGroup = computed(() => props.conversation.type === ImConversationType.GROUP)
+
+/** 鏈�鍚庝竴鏉℃秷鎭彂閫佽�呯殑灞曠ず鍚嶏細瀹炴椂绠� + 蹇収 fallback锛坓etSenderDisplayName 绠椾笉鍑烘椂鍏滃簳锛� */
+const lastSenderDisplayName = computed(() => {
+ const senderId = props.conversation.lastSenderId
+ if (!senderId) {
+ return ''
+ }
+ return getSenderDisplayName(
+ senderId,
+ props.conversation.type,
+ props.conversation.targetId,
+ props.conversation.lastSenderDisplayName
+ )
+})
+
+/** 缇よ亰 + 鏈夋渶鍚庡彂閫佽�� + 鏈�鍚庝竴鏉℃槸鏅�氭秷鎭椂锛屾樉绀哄彂閫佽�呭墠缂�锛團RIEND_* / GROUP_* / RECALL / 鑽夌鎬佷笉甯﹀墠缂�锛� */
+const showSendName = computed(() => {
+ if (draft.value) {
+ return false
+ }
+ if (!isGroup.value) {
+ return false
+ }
+ if (!props.conversation.lastSenderId) {
+ return false
+ }
+ const lastType = props.conversation.lastMessageType
+ return lastType != null && isNormalMessage(lastType)
+})
+
+/** 鍒楄〃灞曠ず鏂囨锛氳崏绋夸紭鍏堬紙瀵归綈寰俊 PC锛氭湁鑽夌鏃剁洊鎺夋渶鍚庝竴鏉¢瑙堬級鈫� 鎾ゅ洖瀹炴椂绠� 鈫� lastContent 鍏滃簳 */
+const lastContentDisplay = computed(() => {
+ if (draft.value) {
+ return draft.value.plain
+ }
+ if (
+ props.conversation.lastMessageType === ImContentType.RECALL &&
+ props.conversation.lastSenderId != null
+ ) {
+ return buildRecallTip(
+ props.conversation.lastSenderId,
+ !!props.conversation.lastSelfSend,
+ props.conversation.type,
+ props.conversation.targetId,
+ props.conversation.lastSenderDisplayName
+ )
+ }
+ return props.conversation.lastContent
+})
+
+/** 浼氳瘽鍒楄〃 "[鑽夌]" / "@ 鎴�" / "@ 鍏ㄤ綋鎴愬憳" 绾㈠瓧鎻愮ず锛涜崏绋夸紭鍏堬紙瀵归綈寰俊 PC锛� */
+const atText = computed(() => {
+ if (draft.value) {
+ return '[鑽夌]'
+ }
+ if (props.conversation.atMe) {
+ return '[鏈変汉@鎴慮'
+ }
+ if (props.conversation.atAll) {
+ return '[@鍏ㄤ綋鎴愬憳]'
+ }
+ return ''
+})
+
+/** 鍏嶆墦鎵颁細璇濇湭璇绘潯鏁版枃妗� */
+const mutedUnreadText = computed(() => {
+ if (!props.conversation.silent || props.conversation.unreadCount <= 0) {
+ return ''
+ }
+ const count = props.conversation.unreadCount > 99 ? '99+' : props.conversation.unreadCount
+ return `[${count}鏉`
+})
+
+/** 缇よ亰鏈鐞嗗姞缇ょ敵璇风孩瀛楀墠缂�锛泂tore 宸茬粡鎸夈�屾垜绠$悊鐨勭兢銆嶈繃婊よ繃锛宑ount > 0 鍗冲彲鏄剧ず */
+const requestText = computed(() => {
+ if (!isGroup.value) {
+ return ''
+ }
+ const count = groupRequestStore.getUnhandledGroupRequestCountMap.get(props.conversation.targetId) ?? 0
+ return count > 0 ? `[${count}鏉¤繘缇ょ敵璇穄` : ''
+})
+
+/** 鐐瑰嚮鍒囦細璇� */
+function handleClick() {
+ conversationStore.setActiveConversation(props.conversation)
+}
+
+/** 鍒囨崲缃《 */
+function handleTop() {
+ conversationStore.setConversationTop(
+ props.conversation.type,
+ props.conversation.targetId,
+ !props.conversation.top
+ )
+}
+
+/** 鍒囨崲鍏嶆墦鎵帮細涔愯 UI锛堝厛鏈湴鍒囨崲锛岃彍鍗曠珛鍗冲叧锛涘悗绔け璐ュ洖婊� conversation 鐘舵�侊級 */
+function handleMuted() {
+ const next = !props.conversation.silent
+ const { type, targetId } = props.conversation
+ conversationStore.setConversationSilent(type, targetId, next)
+ const sync =
+ type === ImConversationType.PRIVATE
+ ? friendStore.setFriendSilent(targetId, next)
+ : groupStore.setGroupSilent(targetId, next)
+ sync.catch((error) => {
+ console.error('[IM] 鍒囨崲鍏嶆墦鎵板け璐�', error)
+ conversationStore.setConversationSilent(type, targetId, !next)
+ })
+}
+
+/** 鍒犻櫎浼氳瘽锛氫簩娆$‘璁ゅ悗杞垹 */
+async function handleDelete() {
+ try {
+ await confirm(`纭畾鍒犻櫎涓庛��${props.conversation.name}銆嶇殑浼氳瘽鍚楋紵`, '鍒犻櫎浼氳瘽')
+ conversationStore.removeConversation(props.conversation.type, props.conversation.targetId)
+ } catch {}
+}
+
+/** 鍙抽敭鑿滃崟锛氱疆椤� / 鍏嶆墦鎵� / 鍒犻櫎 */
+function handleContextMenu(e: MouseEvent) {
+ uiStore.openContextMenu(
+ { x: e.clientX, y: e.clientY },
+ [
+ { key: 'TOP', name: props.conversation.top ? '鍙栨秷缃《' : '缃《' },
+ { key: 'MUTED', name: props.conversation.silent ? '鍏佽娑堟伅閫氱煡' : '娑堟伅鍏嶆墦鎵�' },
+ { key: 'DELETE', name: '鍒犻櫎', divided: true, danger: true }
+ ],
+ (item) => {
+ switch (item.key) {
+ case 'DELETE': {
+ void handleDelete()
+
+ break;
+ }
+ case 'MUTED': {
+ handleMuted()
+
+ break;
+ }
+ case 'TOP': {
+ handleTop()
+
+ break;
+ }
+ // No default
+ }
+ }
+ )
+}
+
+</script>
+
+<template>
+ <div
+ class="relative flex items-center gap-2.5 px-4 py-3 cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
+ :class="{ '!bg-[#d9ecff] dark:!bg-[var(--ant-color-primary-bg-hover)]': isActive }"
+ :data-conversation-key="`${conversation.type}-${conversation.targetId}`"
+ @click="handleClick"
+ @contextmenu.prevent="handleContextMenu"
+ >
+ <!-- 澶村儚 + 鏈鎻愮ず -->
+ <div class="relative">
+ <GroupAvatar
+ v-if="isGroup"
+ :group-id="conversation.targetId"
+ :url="conversation.avatar"
+ :name="conversation.name"
+ :size="40"
+ />
+ <UserAvatar
+ v-else
+ :url="conversation.avatar"
+ :name="conversation.name"
+ :size="40"
+ :clickable="false"
+ />
+ <!-- 鏁板瓧寰芥爣锛氶潪鍏嶆墦鎵颁笖鏈夋湭璇绘椂鏄剧ず鍏蜂綋鏉℃暟 -->
+ <span
+ v-show="!conversation.silent && conversation.unreadCount > 0"
+ class="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1.5 text-11px leading-[18px] text-white text-center bg-[#f56c6c] border border-solid border-white dark:border-[var(--ant-color-bg-container)] rounded-full box-border whitespace-nowrap"
+ >
+ {{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+ </span>
+ <!-- 灏忕孩鐐癸細鍏嶆墦鎵颁笖鏈夋湭璇绘椂鎻愮ず瀛樺湪鏂版秷鎭� -->
+ <span
+ v-show="conversation.silent && conversation.unreadCount > 0"
+ class="absolute -top-1 -right-1 w-2.5 h-2.5 bg-[#f56c6c] border border-solid border-white dark:border-[var(--ant-color-bg-container)] rounded-full box-border"
+ ></span>
+ </div>
+
+ <div class="flex-1 min-w-0">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-1 items-center gap-1 min-w-0">
+ <span class="overflow-hidden text-sm truncate text-[var(--ant-color-text)]">
+ {{ conversation.name }}
+ </span>
+ <Tag
+ v-if="isGroup"
+ type="primary"
+ size="small"
+ class="conversation-item__tag flex-shrink-0 !h-[18px] !px-1 !leading-4"
+ >
+ 缇�
+ </Tag>
+ </span>
+ <span class="flex-shrink-0 ml-1 text-12px text-[var(--ant-color-text-secondary)]">
+ {{ formatConversationTime(conversation.lastSendTime) }}
+ </span>
+ </div>
+ <div class="flex items-center mt-1 leading-5">
+ <!-- 杩涚兢鐢宠绾㈠瓧鍓嶇紑锛氱兢涓� / 绠$悊鍛樼湅鍒拌嚜宸辩鐞嗙殑缇や笅杩樻湁鏈鐞嗙敵璇锋椂鏄剧ず -->
+ <span
+ v-if="requestText"
+ class="conversation-item__prefix flex-shrink overflow-hidden text-12px text-[#c70b0b] truncate whitespace-nowrap"
+ >
+ {{ requestText }}
+ </span>
+ <!-- @绾㈠瓧鎻愮ず锛歛tMe 浼樺厛浜� atAll -->
+ <span
+ v-if="atText"
+ class="conversation-item__prefix flex-shrink overflow-hidden text-12px text-[#c70b0b] truncate whitespace-nowrap"
+ >
+ {{ atText }}
+ </span>
+ <!-- 鍏嶆墦鎵版湭璇绘潯鏁� -->
+ <span
+ v-if="mutedUnreadText"
+ class="flex-shrink-0 mr-1 text-12px text-[var(--ant-color-text-secondary)] whitespace-nowrap"
+ >
+ {{ mutedUnreadText }}
+ </span>
+ <!-- 缇よ亰鏈�鍚庝竴鏉″彂閫佽�呭墠缂�锛氭寜 lastSenderId + 褰撳墠浼氳瘽涓婁笅鏂囧疄鏃剁畻鍚嶅瓧 -->
+ <span
+ v-if="showSendName"
+ class="conversation-item__sender flex-shrink overflow-hidden text-12px text-[var(--ant-color-text-secondary)] truncate whitespace-nowrap"
+ >
+ {{ lastSenderDisplayName }}:
+ </span>
+ <span
+ class="flex-1 min-w-0 overflow-hidden text-12px truncate text-[var(--ant-color-text-secondary)]"
+ >
+ {{ lastContentDisplay }}
+ </span>
+ <!-- 鍏嶆墦鎵板浘鏍� -->
+ <Icon
+ v-if="conversation.silent"
+ icon="mdi:bell-off-outline"
+ :size="14"
+ class="conversation-item__silent flex-shrink-0 ml-1 text-[var(--ant-color-text-disabled)]"
+ title="娑堟伅鍏嶆墦鎵�"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+/* 娑堟帀 el-tag 鍒囦細璇濇椂 active 搴曡壊鍙樺寲鐨勬笎鍙橈紙鐪嬭捣鏉ュ儚闂儊锛夛紱 :deep 绌块�� el-tag 鑷韩鏍峰紡 */
+.conversation-item__tag {
+ transition: none !important;
+}
+
+/* el-icon 鐨勫叏灞� color:var(--color) 鍦ㄦ殫鑹叉ā寮忎笅浼氭覆鏌撴垚鐧借壊锛岃繖閲岀敤 :deep + !important 閿佸畾 */
+.conversation-item__silent :deep(svg) {
+ fill: currentColor !important;
+}
+
+.conversation-item__prefix {
+ max-width: 45%;
+}
+
+.conversation-item__sender {
+ max-width: 50%;
+}
+</style>
--
Gitblit v1.9.3