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

diff --git a/src/views/im/home/pages/conversation/index.vue b/src/views/im/home/pages/conversation/index.vue
new file mode 100644
index 0000000..58517c8
--- /dev/null
+++ b/src/views/im/home/pages/conversation/index.vue
@@ -0,0 +1,281 @@
+<script lang="ts" setup>
+import type { Conversation } from '../../types'
+
+import { computed, nextTick, ref, watch } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { Button, Dropdown, Input, Menu } from 'ant-design-vue'
+
+import { ImConversationType } from '../../../utils/constants'
+import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
+import { StorageKeys } from '../../../utils/db'
+import { getGroupDisplayName } from '../../../utils/user'
+import { ResizableAside } from '../../components'
+import { FriendAddDialog } from '../../components/friend'
+import { GroupCreateDialog } from '../../components/group'
+import { useConversationStore } from '../../store/conversationStore'
+import { useGroupStore } from '../../store/groupStore'
+import { useImUiStore } from '../../store/uiStore'
+import { ConversationItem, MessagePanel } from './components'
+
+defineOptions({ name: 'ImMessagePage' })
+
+const conversationStore = useConversationStore()
+const groupStore = useGroupStore()
+const uiStore = useImUiStore()
+
+// ==================== 浼氳瘽鍒楄〃 ====================
+
+const keyword = ref('')
+
+const sortedConversations = computed(() => conversationStore.getSortedConversationList)
+
+/** 椤堕儴鎼滅储妗嗚繃婊や細璇濓細鍙寜 name 妯$硦鍖归厤锛岄伩鍏嶅懡涓� lastContent 绛夋瑕佸瓧娈靛共鎵� */
+const filteredConversations = computed(() =>
+  filterConversationsByKeyword(sortedConversations.value, keyword.value)
+)
+
+// ==================== 缃《鐩稿叧 ====================
+
+const PINNED_FOLD_THRESHOLD = 3 // 缃《瓒呰繃璇ユ暟閲忔椂鏄剧ず鎶樺彔鍏ュ彛锛涗互涓嬫暟閲忕洿鎺ラ摵寮�锛堥伩鍏嶅崟鏉$疆椤跺氨鍑烘姌鍙犲ご瑙嗚澶噸锛�
+
+// 缃《鎶樺彔灞曞紑鎬侊細localStorage 鎸佷箙鍖栵紝鍒锋柊鍚庝繚鐣欑敤鎴蜂笂娆$殑閫夋嫨锛屽榻愬井淇�
+const pinnedExpanded = ref(
+  localStorage.getItem(StorageKeys.localStorage.conversationPinnedExpanded) === 'true'
+)
+
+/** toggle + 鍐欑洏 */
+function togglePinnedExpanded() {
+  pinnedExpanded.value = !pinnedExpanded.value
+  localStorage.setItem(
+    StorageKeys.localStorage.conversationPinnedExpanded,
+    String(pinnedExpanded.value)
+  )
+}
+
+/** 缃《浼氳瘽锛氬崟鐙垏鐗囷紝缁欐姌鍙犲ご璁℃暟 + 鎶樺彔鍖烘覆鏌撶敤 */
+const pinnedConversations = computed(() => filteredConversations.value.filter((c) => c.top))
+
+/** 闈炵疆椤朵細璇濓細鎶樺彔鎬佷笅濮嬬粓閾哄紑鍦ㄦ姌鍙犲ご涔嬩笅 */
+const normalConversations = computed(() => filteredConversations.value.filter((c) => !c.top))
+
+/**
+ * 缃《鍒嗕袱鍫嗭細visible锛堟姌鍙犲ご涔嬩笂 = 鏈 + 褰撳墠婵�娲伙級/ foldable锛堟姌鍙犲ご涔嬩笅锛夛紱涓�娆� partition 瀹屾垚
+ *
+ * 褰撳墠婵�娲讳細璇濅篃"閽�"鍦� visible锛氶伩鍏嶇偣寮�鏈缃《 鈫� 绔嬪埢琚 鈫� 鍒楄〃涓�闂噸鎺掑洖鎶樺彔鐨勪綋楠�
+ */
+const pinnedGroups = computed(() => {
+  const visible: Conversation[] = []
+  const foldable: Conversation[] = []
+  for (const conversation of pinnedConversations.value) {
+    if (isActiveConversation(conversation) || hasUnreadBadge(conversation)) {
+      visible.push(conversation)
+    } else {
+      foldable.push(conversation)
+    }
+  }
+  return { visible, foldable }
+})
+
+/** 鎶樺彔鏃跺彧娓� visible锛堟湭璇� / 婵�娲荤┛閫忥級锛涘睍寮�鏃舵覆鍏ㄩ儴 鈥斺�� 灞曞紑鍚庝笉鍒嗙粍锛岄伩鍏嶇偣鍑绘姌鍙犲尯璺ㄧ粍涓婅烦 */
+const renderedPinnedConversations = computed(() =>
+  pinnedExpanded.value ? pinnedConversations.value : pinnedGroups.value.visible
+)
+
+/** 缃《鎶樺彔鏃舵槸鍚︿笂娴埌鎶樺彔澶翠箣涓婏細浠呬互鏁板瓧寰芥爣涓哄噯锛涘厤鎵撴壈鍗充究鏈夋湭璇讳篃鍙睍绀哄皬绾㈢偣锛屼笉鍙備笌涓婃诞 */
+function hasUnreadBadge(conversation: Conversation): boolean {
+  return !conversation.silent && (conversation.unreadCount || 0) > 0
+}
+
+/** 鏄惁涓哄綋鍓嶆縺娲讳細璇� */
+function isActiveConversation(conversation: Conversation): boolean {
+  const active = conversationStore.activeConversation
+  return !!active && getConversationKey(active) === getConversationKey(conversation)
+}
+
+/**
+ * 鏄惁杩涘叆"鍒嗙粍妯″紡"锛堢嫭绔嬫祬搴� + 鍙兘鏈夋姌鍙犲ご锛夛細鎼滅储鏃朵笉鍒嗙粍锛堢敤鎴峰湪鎵句汉锛屽埆鍐嶈鎶樺彔鎸′綇锛夛紱
+ * 缃《鏁� < 闃堝�间篃涓嶅垎缁勶紝閬垮厤鍗曟潯缃《灏卞嚭鎶樺彔澶磋瑙夊お閲�
+ */
+const showPinnedSection = computed(
+  () => !keyword.value.trim() && pinnedConversations.value.length >= PINNED_FOLD_THRESHOLD
+)
+
+// ==================== 娣诲姞鏈嬪弸 ====================
+
+const friendAddDialogRef = ref<InstanceType<typeof FriendAddDialog>>() // 娣诲姞鏈嬪弸寮圭獥 ref锛氬彸涓婅 +-涓嬫媺銆屾坊鍔犳湅鍙嬨�嶅叆鍙h皟 open() 瑙﹀彂
+
+// ==================== 寤虹兢鐩稿叧 ====================
+
+const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>() // 鍙戣捣缇よ亰寮圭獥 ref锛歨andleOpenCreateGroup 璋� open() 鎵撳紑
+
+/** 鎵撳紑鍙戣捣缇よ亰寮圭獥锛氭棤閿佸畾椤圭殑鍏ㄥ眬鍏ュ彛 */
+function handleOpenCreateGroup() {
+  createGroupDialogRef.value?.open()
+}
+
+/** 澶勭悊寤虹兢鎴愬姛 */
+function handleGroupCreated(groupId: number) {
+  // GroupCreateDialog 宸茬粡 upsertGroup 鎶婃柊缇ゅ啓杩� store锛岃繖閲屽彧 get + 鎵撳紑浼氳瘽
+  const group = groupStore.getGroup(groupId)
+  if (!group) {
+    return
+  }
+  conversationStore.openConversation(
+    groupId,
+    ImConversationType.GROUP,
+    getGroupDisplayName(group),
+    group.avatar || '',
+    { silent: !!group.silent }
+  )
+}
+
+// ==================== 婊氬姩鍒颁笅涓�涓湭璇� ====================
+// 宸ュ叿鏍忋�屾秷鎭�峊ab 浜屾鐐瑰嚮 鈫� uiStore.nextUnreadJumpNonce 閫掑 鈫� 鏈〉 watch 婊氬姩鍒颁笅涓�鏉℃湭璇�
+// 鍚厤鎵撴壈浼氳瘽锛堝皬绾㈢偣涔熺畻鏈锛夛紱閫氳繃缁存姢 lastJumpedConversationKey 璁╄繛缁偣鍑婚『搴忕┛杩囨暣涓湭璇诲垪琛�
+
+/** 涓婃鍛戒腑鐨勬湭璇讳細璇� key锛涗负绌烘椂浠庢湭璇诲垪琛ㄥご寮�濮� */
+let lastJumpedConversationKey: null | string = null
+
+/** 婊氬姩鍒颁笅涓�涓湭璇讳細璇濓紙鍚厤鎵撴壈锛夛紱鐩爣琚悳绱㈡杩囨护鎴栬棌鍦ㄧ疆椤舵姌鍙犲尯鏃跺厛瑙i櫎鎷︽埅鍐嶆粴 */
+async function jumpToNextUnread() {
+  // 鍚厤鎵撴壈鐨勫叏閲忔湭璇讳細璇濓紱绌哄垯鐩存帴杩斿洖
+  const unreadList = sortedConversations.value.filter((c) => (c.unreadCount || 0) > 0)
+  if (unreadList.length === 0) {
+    return
+  }
+  // 浠庝笂娆″懡涓偅鏉″線鍚庢帹涓�浣嶏紱棣栨鎴栦笂娆$洰鏍囧凡璇诲畬涓嶅湪鍒楄〃閲屾椂锛宺efIndex=-1锛屼粠澶村紑濮�
+  const refIndex = lastJumpedConversationKey
+    ? unreadList.findIndex((c) => getConversationKey(c) === lastJumpedConversationKey)
+    : -1
+  const target = unreadList[(refIndex + 1) % unreadList.length]
+  if (!target) {
+    return
+  }
+  const key = getConversationKey(target)
+  lastJumpedConversationKey = key
+
+  // 鐩爣琚悳绱㈠叧閿瓧杩囨护鎺夛細娓呯┖ keyword 璁╁畠閲嶆柊杩涘叆鍙鍒楄〃
+  if (keyword.value && !filteredConversations.value.some((c) => getConversationKey(c) === key)) {
+    keyword.value = ''
+  }
+  // 鐩爣钘忓湪缃《鎶樺彔鍖猴細涓存椂灞曞紑锛涗笉鍐� localStorage锛屽埛鏂板悗杩樺師鎶樺彔鎬�
+  const inFoldable = pinnedGroups.value.foldable.some((c) => getConversationKey(c) === key)
+  if (inFoldable && !pinnedExpanded.value) {
+    pinnedExpanded.value = true
+  }
+
+  // 绛� keyword / pinnedExpanded 鍙樺寲鍙嶅簲鍒� DOM 鍚庡啀鍘诲彇鐩爣鍏冪礌
+  await nextTick()
+  const el = document.querySelector(`[data-conversation-key="${key}"]`) as HTMLElement | null
+  if (!el) {
+    return
+  }
+  // block: 'start' 鎶婄洰鏍囦細璇濋《鍒板垪琛ㄥ彲瑙嗗尯绗竴琛岋紱瀵归綈寰俊"鍒囧埌涓嬩竴涓湭璇�=鍒楄〃鐨勭涓�鏉�"
+  el.scrollIntoView({ behavior: 'smooth', block: 'start' })
+}
+
+watch(() => uiStore.nextUnreadJumpNonce, jumpToNextUnread)
+</script>
+
+<template>
+  <!-- 娑堟伅 Tab锛氬乏渚т細璇濆垪琛� + 鍙充晶鑱婂ぉ闈㈡澘 -->
+  <div class="flex flex-1 min-w-0 h-full">
+    <!-- 宸︿晶浼氳瘽鍒楄〃锛堝彲鎷栨嫿瀹藉害锛� -->
+    <ResizableAside :default-width="260" :storage-key="StorageKeys.localStorage.asideWidth">
+      <!-- 椤堕儴锛氭悳绱㈡ + "+" 鍙蜂笅鎷夛紙瀵归綈寰俊 PC锛氬彂璧风兢鑱� / 娣诲姞鏈嬪弸锛夛紱h-14 涓庡彸渚� MessagePanel 澶撮儴瀵归綈 -->
+      <div
+        class="flex flex-shrink-0 gap-2 items-center h-14 px-4 border-b border-b-solid border-[var(--im-border-color-lighter)]"
+      >
+        <Input v-model:value="keyword" placeholder="鎼滅储" allow-clear class="flex-1">
+          <template #prefix>
+            <Icon icon="ant-design:search-outlined" />
+          </template>
+        </Input>
+        <Dropdown :trigger="['click']" placement="bottom">
+          <Button size="small" shape="circle">
+            <Icon icon="ant-design:plus-outlined" />
+          </Button>
+          <template #overlay>
+            <Menu>
+              <Menu.Item @click="handleOpenCreateGroup">
+                <span class="inline-flex items-center gap-2">
+                  <Icon icon="ant-design:message-outlined" :size="16" />
+                  <span>鍙戣捣缇よ亰</span>
+                </span>
+              </Menu.Item>
+              <Menu.Item @click="friendAddDialogRef?.open()">
+                <span class="inline-flex items-center gap-2">
+                  <Icon icon="ant-design:user-add-outlined" :size="16" />
+                  <span>娣诲姞鏈嬪弸</span>
+                </span>
+              </Menu.Item>
+            </Menu>
+          </template>
+        </Dropdown>
+      </div>
+
+      <!-- 浼氳瘽鍒楄〃涓讳綋 -->
+      <div class="flex-1 overflow-y-auto">
+        <!-- 缃《浼氳瘽锛氭暟閲� < 闃堝�兼垨鎼滅储涓洿鎺ラ摵寮�锛涘惁鍒欒繘鍏ュ垎缁勬ā寮忥紙鐙珛娴呭簳 + 鍙�夋姌鍙犲ご锛� -->
+        <template v-if="!showPinnedSection">
+          <ConversationItem
+            v-for="conversation in pinnedConversations"
+            :key="getConversationKey(conversation)"
+            :conversation="conversation"
+          />
+        </template>
+        <div v-else class="bg-[var(--ant-color-fill-secondary)]">
+          <ConversationItem
+            v-for="conversation in renderedPinnedConversations"
+            :key="getConversationKey(conversation)"
+            :conversation="conversation"
+          />
+
+          <!-- 鎶樺彔澶达細鏀惧湪缃《鍖哄簳閮ㄥ榻� WeChat mac锛涘睍寮� / 鎶樺彔鎬佸叡鐢紝浠呭湪杩樻湁"鍙姌鍙�"鍐呭銆佹垨褰撳墠宸插睍寮�鏃跺嚭鐜� -->
+          <div
+            v-if="pinnedGroups.foldable.length > 0 || pinnedExpanded"
+            class="flex items-center justify-between px-4 py-2.5 cursor-pointer transition-colors text-13px text-[var(--ant-color-text)] border-y border-y-solid border-[var(--im-border-color-lighter)] hover:bg-[var(--ant-color-fill)]"
+            @click="togglePinnedExpanded"
+          >
+            <span class="flex items-center gap-1.5">
+              <Icon icon="ant-design:menu-outlined" :size="14" />
+              {{
+                pinnedExpanded
+                  ? '鎶樺彔缃《鑱婂ぉ'
+                  : `${pinnedGroups.foldable.length} 涓疆椤惰亰澶ー
+              }}
+            </span>
+            <Icon
+              :icon="pinnedExpanded ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
+              :size="11"
+              class="text-[var(--ant-color-text-placeholder)]"
+            />
+          </div>
+        </div>
+
+        <!-- 鏅�氫細璇� -->
+        <ConversationItem
+          v-for="conversation in normalConversations"
+          :key="getConversationKey(conversation)"
+          :conversation="conversation"
+        />
+
+        <div
+          v-if="filteredConversations.length === 0"
+          class="flex items-center justify-center py-10 text-sm text-[var(--ant-color-text-secondary)]"
+        >
+          {{ keyword ? '娌℃湁婊¤冻鏉′欢鐨勪細璇�' : '鏆傛棤浼氳瘽' }}
+        </div>
+      </div>
+    </ResizableAside>
+
+    <!-- 鍙充晶鑱婂ぉ闈㈡澘 -->
+    <MessagePanel />
+
+    <!-- 娣诲姞鏈嬪弸 / 鍙戣捣缇よ亰寮圭獥 -->
+    <FriendAddDialog ref="friendAddDialogRef" />
+    <GroupCreateDialog ref="createGroupDialogRef" @created="handleGroupCreated" />
+  </div>
+</template>

--
Gitblit v1.9.3