diff --git a/src/views/im/home/pages/conversation/index.vue b/src/views/im/home/pages/conversation/index.vue
index f8a85e481..6d39c6161 100644
--- a/src/views/im/home/pages/conversation/index.vue
+++ b/src/views/im/home/pages/conversation/index.vue
@@ -33,11 +33,50 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ pinnedExpanded
+ ? '折叠置顶聊天'
+ : `${foldablePinnedConversations.length} 个置顶聊天`
+ }}
+
+
+
+
+
+
+
{
)
})
+// ==================== 置顶相关 ====================
+
+/** 置顶超过该数量时显示折叠入口;以下数量直接铺开(避免单条置顶就出折叠头视觉太重) */
+const PINNED_FOLD_THRESHOLD = 3
+
+/** 置顶折叠展开态:localStorage 持久化,刷新后保留用户上次的选择,对齐微信 */
+const pinnedExpanded = ref(
+ localStorage.getItem(StorageKeys.conversationPinnedExpanded) === 'true'
+)
+
+/** toggle + 写盘 */
+function togglePinnedExpanded() {
+ pinnedExpanded.value = !pinnedExpanded.value
+ localStorage.setItem(StorageKeys.conversationPinnedExpanded, String(pinnedExpanded.value))
+}
+
+/** 置顶会话:单独切片,给折叠头计数 + 折叠区渲染用 */
+const pinnedConversations = computed(() => filteredConversations.value.filter((c) => c.top))
+
+/** 非置顶会话:折叠态下始终铺开在折叠头之下 */
+const normalConversations = computed(() => filteredConversations.value.filter((c) => !c.top))
+
+/** 置顶 + 无未读 / 免打扰且非激活:折叠时藏在折叠头之下,决定折叠头计数 + 是否要显示折叠头 */
+const foldablePinnedConversations = computed(() =>
+ pinnedConversations.value.filter((c) => !isActiveConversation(c) && !hasUnreadBadge(c))
+)
+
+/**
+ * 折叠时只渲未读 + 当前激活(穿透折叠);展开时渲全部置顶
+ *
+ * 展开后不沿用「visible 在前 + foldable 在后」的分组:会让点击折叠区某条跨组上跳、
+ * 上一条激活的会从 visible 掉到 foldable,视觉上像"互换位置"——按 lastSendTime 自然顺序铺最稳
+ */
+const renderedPinnedConversations = computed(() => {
+ if (pinnedExpanded.value) {
+ return pinnedConversations.value
+ }
+ return pinnedConversations.value.filter((c) => isActiveConversation(c) || hasUnreadBadge(c))
+})
+
+/** 与会话项右上角红点的可见条件保持一致:免打扰不亮,无未读不亮 */
+function hasUnreadBadge(conversation: Conversation): boolean {
+ return !conversation.muted && (conversation.unreadCount || 0) > 0
+}
+
+/** 是否为当前激活会话 */
+function isActiveConversation(conversation: Conversation): boolean {
+ const active = conversationStore.activeConversation
+ return !!active && active.type === conversation.type && active.targetId === conversation.targetId
+}
+
+/**
+ * 是否进入"分组模式"(独立浅底 + 可能有折叠头):搜索时不分组(用户在找人,别再让折叠挡住);
+ * 置顶数 < 阈值也不分组,避免单条置顶就出折叠头视觉太重
+ */
+const showPinnedSection = computed(
+ () => !keyword.value.trim() && pinnedConversations.value.length >= PINNED_FOLD_THRESHOLD
+)
+
+// ==================== 建群相关 ====================
+
/** GroupCreateDialog 需要全量好友列表来勾选成员,结构与通讯录里好友/群分组保持一致 */
const friends = computed(() =>
friendStore.getActiveFriends.map((friend: Friend) => ({
@@ -116,7 +216,6 @@ function handleGroupCreated(groupId: number) {
if (!group) {
return
}
- // 打开会话
conversationStore.openConversation(
groupId,
ImConversationType.GROUP,
diff --git a/src/views/im/manager/friend/index.vue b/src/views/im/manager/friend/index.vue
index 7264eed53..fda62d850 100644
--- a/src/views/im/manager/friend/index.vue
+++ b/src/views/im/manager/friend/index.vue
@@ -8,25 +8,39 @@
:inline="true"
label-width="80px"
>
-
-
-
+
+
+ >
+
+
-
-
-
+
+
+ >
+
+
-
-
+
+
{{ row.userNickname || '-' }}
({{ row.userId }})
-
-
+
+
{{ row.friendNickname || '-' }}
({{ row.friendUserId }})
@@ -134,12 +148,14 @@
import { dateFormatter } from '@/utils/formatTime'
import { DICT_TYPE, getIntDictOptions, getBoolDictOptions } from '@/utils/dict'
import * as ManagerFriendApi from '@/api/im/manager/friend'
+import * as UserApi from '@/api/system/user'
defineOptions({ name: 'ImFriend' })
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
+const userOptions = ref([]) // 用户下拉的候选项
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@@ -176,7 +192,9 @@ const resetQuery = () => {
}
/** 初始化 */
-onMounted(() => {
- getList()
+onMounted(async () => {
+ // 用户下拉一次性拉简化数据,给 userId / friendUserId 共用
+ userOptions.value = await UserApi.getSimpleUserList()
+ await getList()
})
diff --git a/src/views/im/manager/message/group/index.vue b/src/views/im/manager/message/group/index.vue
index 5984ad3d0..f86e84d91 100644
--- a/src/views/im/manager/message/group/index.vue
+++ b/src/views/im/manager/message/group/index.vue
@@ -172,7 +172,7 @@
import { dateFormatter, formatDate } from '@/utils/formatTime'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as ManagerGroupMessageApi from '@/api/im/manager/message/group'
-import { getContentPreview, formatJson } from '../utils'
+import { getContentPreview, formatJson } from '@/views/im/utils/message'
defineOptions({ name: 'ImGroupMessage' })
diff --git a/src/views/im/manager/message/private/index.vue b/src/views/im/manager/message/private/index.vue
index 844f92470..8e5f0d3bd 100644
--- a/src/views/im/manager/message/private/index.vue
+++ b/src/views/im/manager/message/private/index.vue
@@ -159,7 +159,7 @@
import { dateFormatter, formatDate } from '@/utils/formatTime'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as ManagerPrivateMessageApi from '@/api/im/manager/message/private'
-import { getContentPreview, formatJson } from '../utils'
+import { getContentPreview, formatJson } from '@/views/im/utils/message'
defineOptions({ name: 'ImPrivateMessage' })
diff --git a/src/views/im/manager/message/utils.ts b/src/views/im/manager/message/utils.ts
deleted file mode 100644
index 141b8dd46..000000000
--- a/src/views/im/manager/message/utils.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// TODO @AI:应该放到 utils 里
-// IM 消息管理共享工具:消息内容预览 / JSON 美化
-//
-// 消息类型 / 消息状态都走字典:
-// DICT_TYPE.IM_MESSAGE_TYPE - 对应后端 ImMessageTypeEnum
-// DICT_TYPE.IM_MESSAGE_STATUS - 对应后端 ImMessageStatusEnum
-
-/** 消息内容(JSON)取首层 content 字段做列表预览,解析失败时回退原文 */
-export const getContentPreview = (content?: string): string => {
- if (!content) return ''
- try {
- const parsed = JSON.parse(content)
- if (typeof parsed === 'object' && parsed.content) return String(parsed.content)
- return content
- } catch {
- return content
- }
-}
-
-/** 详情弹窗里把 content JSON 美化成 2 缩进 */
-export const formatJson = (content?: string): string => {
- if (!content) return ''
- try {
- return JSON.stringify(JSON.parse(content), null, 2)
- } catch {
- return content
- }
-}
diff --git a/src/views/im/utils/message.ts b/src/views/im/utils/message.ts
index e7e90833d..4c26830d2 100644
--- a/src/views/im/utils/message.ts
+++ b/src/views/im/utils/message.ts
@@ -151,3 +151,27 @@ export const playAudioTip = () => {
console.debug('[IM] playAudioTip 失败', e)
}
}
+
+// ==================== 管理后台展示工具 ====================
+
+/** 消息内容(JSON)取首层 content 字段做列表预览,解析失败时回退原文 */
+export const getContentPreview = (content?: string): string => {
+ if (!content) return ''
+ try {
+ const parsed = JSON.parse(content)
+ if (typeof parsed === 'object' && parsed.content) return String(parsed.content)
+ return content
+ } catch {
+ return content
+ }
+}
+
+/** 详情弹窗里把 content JSON 美化成 2 缩进 */
+export const formatJson = (content?: string): string => {
+ if (!content) return ''
+ try {
+ return JSON.stringify(JSON.parse(content), null, 2)
+ } catch {
+ return content
+ }
+}