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/message/group-pinned-message.vue |  207 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 207 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue b/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
new file mode 100644
index 0000000..b411944
--- /dev/null
+++ b/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
@@ -0,0 +1,207 @@
+<script lang="ts" setup>
+import type { Message } from '../../../../types'
+
+import { computed, ref, watch } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { Button, message } from 'ant-design-vue'
+
+import { unpinGroupMessage as apiUnpinGroupMessage } from '#/api/im/group'
+import { getCurrentUserId } from '#/views/im/utils/auth'
+import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants'
+import { resolveConversationLastContent } from '#/views/im/utils/conversation'
+import { getSenderDisplayName, isGroupQuit } from '#/views/im/utils/user'
+
+import { useGroupStore } from '../../../../store/groupStore'
+
+defineOptions({ name: 'ImGroupPinnedMessage' })
+
+const props = defineProps<{
+  /** 褰撳墠缇ょ紪鍙凤紙鑷浠� groupStore 鎷垮畬鏁� Group锛岃窡闅忓搷搴斿紡锛� */
+  groupId: number
+}>()
+
+const emit = defineEmits<{
+  /** 鐐瑰嚮缃《娑堟伅 鈫� 鐖剁骇 MessagePanel 婊氬姩瀹氫綅鍒板師娑堟伅浣嶇疆 */
+  locate: [messageId: number]
+}>()
+
+const groupStore = useGroupStore()
+
+/** 褰撳墠缇わ紙鍚� pinnedMessages锛� */
+const group = computed(() => groupStore.getGroup(props.groupId))
+
+const expanded = ref(false)
+const removingId = ref<null | number>(null)
+
+// 鍒囩兢鏃堕噸缃睍寮� / 绉婚櫎涓姸鎬侊細鏈湴 ref 涓嶈窡闅� groupId锛屽惁鍒欎笂涓�缇�"灞曞紑"鎴�"绉婚櫎涓�"浼氬甫鍒版柊缇�
+watch(
+  () => props.groupId,
+  () => {
+    expanded.value = false
+    removingId.value = null
+  }
+)
+
+/** 褰撳墠缇ょ疆椤舵秷鎭垪琛紙鐩存帴璧� group.value锛岃窡闅忓搷搴斿紡锛� */
+const pinnedMessages = computed<Message[]>(() => group.value?.pinnedMessages ?? [])
+
+/** 椤堕儴鑳跺泭灞曠ず鐨勬渶鏂颁竴鏉★紙鍗冲垪琛ㄦ渶鍚庝竴鏉★紝pin 椤哄簭杩藉姞锛� */
+const latest = computed<Message | null>(
+  () => pinnedMessages.value[pinnedMessages.value.length - 1] ?? null
+)
+
+/** 褰撳墠鐢ㄦ埛鏄惁缇や富 / 绠$悊鍛橈紙鍐冲畾鏄惁鏄剧ず銆岀Щ闄ゃ�嶅叆鍙o級 */
+const canManage = computed(() => {
+  // 鍘嗗彶閫�缇ょ兢锛氭湰鍦扮紦瀛樻畫鐣欐椂涔熶笉缁欍�岀Щ闄ゃ�嶅叆鍙�
+  if (isGroupQuit(group.value)) {
+    return false
+  }
+  const myId = getCurrentUserId()
+  const role = group.value?.members?.find((m) => m.userId === myId)?.role
+  return role === ImGroupMemberRole.OWNER || role === ImGroupMemberRole.ADMIN
+})
+
+/** 椤堕儴鑳跺泭鐐瑰嚮锛氬崟鏉$洿鎺ヨ烦杞師娑堟伅浣嶇疆锛涘鏉″垏鎹㈠睍寮� / 鎶樺彔 */
+function handleTopClick() {
+  if (!latest.value) {
+    return
+  }
+  if (pinnedMessages.value.length === 1) {
+    handleLocate(latest.value)
+    return
+  }
+  expanded.value = !expanded.value
+}
+
+/** 鐐瑰嚮缃《娑堟伅琛� 鈫� 瑙﹀彂璺宠浆 + 鏀惰捣寮瑰嚭灞� */
+function handleLocate(message: Message) {
+  if (!message.id) {
+    return
+  }
+  emit('locate', message.id)
+  expanded.value = false
+}
+
+/** 缃《娑堟伅鍙戦�佷汉鏄剧ず鍚� */
+function getSenderName(message: Message): string {
+  return group.value
+    ? getSenderDisplayName(message.senderId, ImConversationType.GROUP, group.value.id)
+    : ''
+}
+
+/** 缃《娑堟伅棰勮鏂囨湰锛氬鐢ㄤ細璇濇渶鍚庝竴鏉℃憳瑕侀�昏緫锛圼鍥剧墖] / [鏂囦欢] / 鏂囨湰绛夛級 */
+function getPreview(message: Message): string {
+  return group.value
+    ? resolveConversationLastContent(message, ImConversationType.GROUP, group.value.id)
+    : ''
+}
+
+/** 绉婚櫎缃《锛氳皟鍚庣 API锛宭oading 鏈熼棿绂佹閲嶅鐐癸紱鍚庣骞挎挱 GROUP_MESSAGE_UNPIN 鐢� dispatcher 鑷姩鍚屾鏈湴 */
+async function handleRemove(pinnedMessage: Message) {
+  if (!group.value || !pinnedMessage.id || removingId.value !== null) {
+    return
+  }
+  removingId.value = pinnedMessage.id
+  try {
+    await apiUnpinGroupMessage({ id: group.value.id, messageId: pinnedMessage.id })
+    message.success('宸插彇娑堢疆椤�')
+  } finally {
+    removingId.value = null
+  }
+}
+</script>
+
+<template>
+  <!-- 缇よ亰缃《娑堟伅锛氫粎缇よ亰 + 鏈夌疆椤舵椂鏄剧ず锛屾偓鎸傚湪缇よ亰澶撮儴涓嬫柟宸︿笂瑙掞紱涓嶅崰鏁磋锛堝榻愬井淇� PC锛� -->
+  <div
+    v-if="latest"
+    class="im-group-pinned-message relative flex flex-shrink-0 flex-col items-start px-4 pt-1.5 pb-2 bg-[var(--ant-color-fill-secondary)]"
+  >
+    <!-- 椤堕儴鑳跺泭锛氬崟鏉$偣鍑昏烦杞紱澶氭潯鎶樺彔鐐瑰嚮灞曞紑锛涘鏉″睍寮�鐐瑰嚮鎶樺彔 -->
+    <div
+      class="flex items-center gap-1.5 w-[360px] px-3 py-1.5 rounded-[10px] text-13px text-[var(--ant-color-text)] bg-[var(--ant-color-bg-container)] shadow-[0_1px_2px_rgba(0,0,0,0.04)] cursor-pointer hover:bg-[var(--ant-color-fill-tertiary)]"
+      @click="handleTopClick"
+    >
+      <Icon
+        icon="ant-design:pushpin-outlined"
+        :size="14"
+        class="flex-shrink-0 text-[var(--ant-color-warning)]"
+      />
+      <span class="flex-shrink-0 text-[var(--ant-color-text-secondary)]">{{ getSenderName(latest) }}锛�</span>
+      <span class="flex-1 min-w-0 truncate">{{ getPreview(latest) }}</span>
+      <!-- 鍗曟潯锛氱Щ闄ゆ寜閽紱澶氭潯鎶樺彔锛氬叡 N 鏉★紱澶氭潯灞曞紑锛氭敹璧风澶� -->
+      <Button
+        v-if="pinnedMessages.length === 1 && canManage"
+        type="link"
+        size="small"
+        :loading="removingId === latest.id"
+        class="flex-shrink-0 !h-auto !p-0 text-13px"
+        @click.stop="handleRemove(latest)"
+      >
+        绉婚櫎
+      </Button>
+      <template v-else-if="pinnedMessages.length > 1">
+        <span class="flex-shrink-0 text-[var(--ant-color-text-secondary)] text-12px">
+          鍏� {{ pinnedMessages.length }} 鏉�
+        </span>
+        <Icon
+          :icon="expanded ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
+          :size="11"
+          class="flex-shrink-0 text-[var(--ant-color-text-placeholder)]"
+        />
+      </template>
+    </div>
+
+    <!-- 澶氭潯灞曞紑锛氭祬鑹查潰鏉垮寘瑁瑰畬鏁村垪琛紝姣忔潯鐙珛鑳跺泭锛涚偣鍑昏烦杞埌瀵瑰簲娑堟伅浣嶇疆 -->
+    <div
+      v-if="pinnedMessages.length > 1 && expanded"
+      class="im-group-pinned-message__list absolute top-full left-1.5 z-10 flex flex-col gap-2.5 w-[380px] p-3 rounded-xl bg-[var(--ant-color-bg-container)] shadow-[0_6px_16px_rgba(0,0,0,0.12)]"
+      style="margin-top: -1px"
+    >
+      <div
+        v-for="msg in pinnedMessages"
+        :key="msg.id"
+        class="flex items-center gap-1.5 w-full px-3 py-1.5 rounded-[10px] text-13px text-[var(--ant-color-text)] bg-[var(--ant-color-fill-secondary)] cursor-pointer hover:bg-[var(--ant-color-bg-container)]"
+        @click="handleLocate(msg)"
+      >
+        <Icon
+          icon="ant-design:pushpin-outlined"
+          :size="14"
+          class="flex-shrink-0 text-[var(--ant-color-warning)]"
+        />
+        <span class="flex-shrink-0 text-[var(--ant-color-text-secondary)]">
+          {{ getSenderName(msg) }}锛�
+        </span>
+        <span class="flex-1 min-w-0 truncate">{{ getPreview(msg) }}</span>
+        <Button
+          v-if="canManage"
+          type="link"
+          size="small"
+          :loading="removingId === msg.id"
+          class="flex-shrink-0 !h-auto !p-0 text-13px"
+          @click.stop="handleRemove(msg)"
+        >
+          绉婚櫎
+        </Button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* 寮瑰嚭灞傛湞涓婄殑涓夎绠ご锛涜蛋 ::before + 4 杈� border 閰嶈壊鐢伙紝棰滆壊璺熷脊鍑哄眰 background 涓�鑷� */
+.im-group-pinned-message__list::before {
+  content: '';
+  position: absolute;
+  top: -8px;
+  left: 184px;
+  width: 0;
+  height: 0;
+  border-left: 8px solid transparent;
+  border-right: 8px solid transparent;
+  border-bottom: 8px solid var(--ant-color-bg-container);
+  filter: drop-shadow(0 -2px 1px rgba(0, 0, 0, 0.04));
+}
+</style>

--
Gitblit v1.9.3