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-private-side.vue |  285 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 285 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/pages/conversation/components/conversation/conversation-private-side.vue b/src/views/im/home/pages/conversation/components/conversation/conversation-private-side.vue
new file mode 100644
index 0000000..7939da1
--- /dev/null
+++ b/src/views/im/home/pages/conversation/components/conversation/conversation-private-side.vue
@@ -0,0 +1,285 @@
+<script lang="ts" setup>
+import type { Conversation, Friend } from '../../../../types'
+
+import { computed, ref, watch } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { Button, Drawer, Input, message, Popover, Spin, Switch } from 'ant-design-vue'
+
+import { useConversationStore } from '#/views/im/home/store/conversationStore'
+import { useFriendStore } from '#/views/im/home/store/friendStore'
+import { useGroupStore } from '#/views/im/home/store/groupStore'
+import { ImConversationType } from '#/views/im/utils/constants'
+import { getFriendDisplayName, getGroupDisplayName } from '#/views/im/utils/user'
+
+import { GroupCreateDialog } from '../../../../components/group'
+import { UserAvatar } from '../../../../components/user'
+
+defineOptions({ name: 'ImConversationPrivateSide' })
+
+const props = withDefaults(
+  defineProps<{
+    conversation?: Conversation | null // 褰撳墠浼氳瘽锛堝彇缃《 / 鍏嶆墦鎵版�侊級
+    friend?: Friend // 瀵规柟濂藉弸淇℃伅锛堝彇澶村儚 / 鏄电О锛�
+    modelValue?: boolean // 鎶藉眽寮�鍏筹紙v-model锛�
+  }>(),
+  {
+    conversation: null,
+    friend: undefined,
+    modelValue: false
+  }
+)
+
+const emit = defineEmits<{
+  openHistory: [] // 鐐瑰嚮 "鏌ユ壘鑱婂ぉ鍐呭" 琛� 鈫� 鐖剁粍浠舵墦寮� MessageHistory 寮圭獥
+  'update:modelValue': [value: boolean]
+}>()
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+const conversationStore = useConversationStore()
+const friendStore = useFriendStore()
+const groupStore = useGroupStore()
+
+/** tile 鏍囩 / 鍚庣画鑱婂ぉ鐣岄潰鐢ㄧ殑灞曠ず鍚嶏細澶囨敞浼樺厛 */
+const displayName = computed(() => (props.friend ? getFriendDisplayName(props.friend) : ''))
+
+const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>() // 鍙戣捣缇よ亰寮圭獥 ref锛歨andleOpenCreateGroup 璋� open({ lockedIds }) 閿佸畾瀵规柟
+
+/** 鎵撳紑鍙戣捣缇よ亰寮圭獥锛氭妸瀵规柟榛樿鍕句笂涓斾笉鍙彇娑堬紝瀵瑰簲寰俊"鍩轰簬绉佽亰鍙戣捣缇よ亰" */
+function handleOpenCreateGroup() {
+  const lockedIds = props.friend ? [props.friend.friendUserId] : []
+  createGroupDialogRef.value?.open({ lockedIds })
+}
+
+const displayNamePopoverVisible = ref(false)
+const editDisplayName = ref('')
+
+// popover 寮瑰嚭鏃舵妸褰撳墠澶囨敞鐏岃繘缂栬緫鎬侊紝閬垮厤涓婃鏈繚瀛樼殑鑴忓��
+watch(displayNamePopoverVisible, (open) => {
+  if (open) {
+    editDisplayName.value = props.friend?.displayName || ''
+  }
+})
+
+// 鎶藉眽鍏抽棴鏃舵妸杩樻病鏀舵帀鐨� popover 涓�骞舵竻鎺夛紝閬垮厤涓嬫鎵撳紑闂竴涓�
+watch(visible, (open) => {
+  if (!open) {
+    displayNamePopoverVisible.value = false
+  }
+})
+
+/** 澶囨敞 popover 鐐瑰嚮淇濆瓨 */
+async function handleSaveDisplayName() {
+  if (!props.friend) {
+    return
+  }
+  await friendStore.setFriendDisplayName(props.friend.friendUserId, editDisplayName.value)
+  displayNamePopoverVisible.value = false
+  message.success('淇濆瓨鎴愬姛')
+}
+
+/**
+ * 鍒囧厤鎵撴壈锛氫箰瑙傚弻鍐� conversationStore + friendStore锛涘悗绔け璐ュ洖婊� conversation 鐘舵�侊紝淇濇寔涓� ConversationItem.handleMuted 涓�鑷�
+ */
+function handleMutedChange(value: boolean | number | string) {
+  if (!props.conversation) {
+    return
+  }
+  const next = !!value
+  const { type, targetId } = props.conversation
+  conversationStore.setConversationSilent(type, targetId, next)
+  if (type !== ImConversationType.PRIVATE) {
+    return
+  }
+  friendStore.setFriendSilent(targetId, next).catch((error) => {
+    console.error('[IM ConversationPrivateSide] 鍒囨崲鍏嶆墦鎵板け璐�', { targetId }, error)
+    conversationStore.setConversationSilent(type, targetId, !next)
+  })
+}
+
+/** 鍒囩疆椤讹細绾湰鍦� conversationStore 鎺掑簭鎬侊紙鏃犲悗绔瓧娈碉級 */
+function handleTopChange(value: boolean | number | string) {
+  if (!props.conversation) {
+    return
+  }
+  conversationStore.setConversationTop(props.conversation.type, props.conversation.targetId, !!value)
+}
+
+/** 缇ゅ垱寤烘垚鍔燂細璺冲埌鏂扮兢浼氳瘽 + 鍏虫帀鏈晶鎶藉眽锛岃鐢ㄦ埛涓撴敞鏂扮兢 */
+function handleGroupCreated(groupId: number) {
+  const group = groupStore.getGroup(groupId)
+  if (!group) {
+    return
+  }
+  conversationStore.openConversation(
+    groupId,
+    ImConversationType.GROUP,
+    getGroupDisplayName(group),
+    group.avatar || '',
+    { silent: !!group.silent }
+  )
+  visible.value = false
+}
+</script>
+
+<template>
+  <!--
+    绉佽亰渚ц竟鎶藉眽
+    - 鏁翠綋缁撴瀯瀵归綈 ConversationGroupSide锛氬鏍� + 淇℃伅琛� + 寮�鍏�
+    - 椤堕儴濂藉弸瀹牸 + "+" tile锛氱偣 + 璋冭捣 GroupCreateDialog 骞堕攣瀹氬鏂癸紝瀵归綈寰俊"鍩轰簬绉佽亰鍙戣捣缇よ亰"
+    - "娓呯┖鑱婂ぉ璁板綍"鎸夐挳鍦� WeChat 閲屾湁锛屼絾鐩墠鍚庣娌″缓娑堟伅娓呯┖鑳藉姏锛屽厛涓嶅姞閬垮厤鍋氬崐鍚婂瓙
+  -->
+  <Drawer
+    v-model:open="visible"
+    :closable="false"
+    placement="right"
+    width="340px"
+    root-class-name="im-conversation-private-side__modal"
+  >
+    <!-- friend 缂哄け鍦烘櫙锛氶檶鐢熶汉浼氳瘽鍒氭墦寮� / 濂藉弸鏁版嵁杩樻病琛ユ媺鍒帮紱绌虹櫧浼氳鐢ㄦ埛浠ヤ负鎶藉眽鍧忎簡锛岀粰涓姞杞藉崰浣� -->
+    <div
+      v-if="!friend"
+      class="flex flex-col items-center justify-center h-full text-13px text-[var(--ant-color-text-placeholder)] bg-[var(--ant-color-bg-container)]"
+    >
+      <Spin tip="鍔犺浇涓�..." />
+    </div>
+    <div v-else class="flex flex-col h-full bg-[var(--ant-color-bg-container)]">
+      <div class="flex-1 overflow-y-auto bg-[var(--ant-color-fill-secondary)]">
+        <!-- 濂藉弸瀹牸锛氬師 tile + "+" tile锛屽榻� GroupSide 瑙嗚锛岃涓ょ鎶藉眽鐪嬭捣鏉ユ槸涓�瀹剁殑 -->
+        <div class="flex flex-wrap gap-1 px-4 pt-4 pb-[14px] bg-[var(--ant-color-bg-container)]">
+          <div class="flex flex-col items-center w-[66px]">
+            <UserAvatar
+              :id="friend.friendUserId"
+              :url="friend.avatar"
+              :name="friend.nickname"
+              :size="50"
+            />
+            <div class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--ant-color-text)] text-center truncate">
+              {{ displayName }}
+            </div>
+          </div>
+
+          <!-- + tile锛氱偣鍑昏皟璧� GroupCreateDialog锛屾妸瀵规柟 id 浣滀负 lockedIds 浼犲叆 -->
+          <div
+            class="im-conversation-private-side__tile-wrap-clickable flex flex-col items-center w-[66px] cursor-pointer"
+            title="鍙戣捣缇よ亰"
+            @click="handleOpenCreateGroup"
+          >
+            <div class="im-conversation-private-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--ant-color-text)] bg-[var(--ant-color-fill-tertiary)] border border-dashed border-[var(--ant-color-border)] rounded-md transition-colors duration-200">
+              <Icon icon="ant-design:plus-outlined" />
+            </div>
+            <div class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--ant-color-text)] text-center truncate">娣诲姞</div>
+          </div>
+        </div>
+
+        <div class="flex-shrink-0 h-[10px]"></div>
+
+        <!-- 澶囨敞锛堜粎鑷繁鍙锛夛細鐐瑰嚮寮� popover 缂栬緫锛屼繚瀛樺悗绔嬪嵆鍒锋柊鏈娊灞� + 浼氳瘽鍒楄〃灞曠ず鍚� -->
+        <div class="bg-[var(--ant-color-bg-container)]">
+          <Popover
+            v-model:open="displayNamePopoverVisible"
+            trigger="click"
+            placement="leftTop"
+            :overlay-style="{ width: '280px' }"
+          >
+            <div
+              class="im-conversation-private-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
+            >
+              <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">澶囨敞</span>
+              <span
+                v-if="friend.displayName"
+                class="text-13px leading-[1.6] text-[var(--ant-color-text)] break-all line-clamp-2"
+              >
+                {{ friend.displayName }}
+              </span>
+              <span v-else class="text-13px leading-[1.6] text-[var(--ant-color-text-placeholder)]">
+                濂藉弸澶囨敞浠呰嚜宸卞彲瑙�
+              </span>
+            </div>
+            <template #content>
+              <div class="flex flex-col gap-2">
+                <Input
+                  v-model:value="editDisplayName"
+                  :maxlength="16"
+                  show-count
+                  placeholder="璇疯緭鍏ュ娉ㄥ悕"
+                />
+                <div class="flex justify-end gap-2">
+                  <Button size="small" @click="displayNamePopoverVisible = false">鍙栨秷</Button>
+                  <Button size="small" type="primary" @click="handleSaveDisplayName">
+                    淇濆瓨
+                  </Button>
+                </div>
+              </div>
+            </template>
+          </Popover>
+        </div>
+
+        <div class="flex-shrink-0 h-[10px]"></div>
+
+        <!-- 鏌ユ壘鑱婂ぉ鍐呭 -->
+        <div class="bg-[var(--ant-color-bg-container)]">
+          <div
+            class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
+            @click="emit('openHistory')"
+          >
+            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">鏌ユ壘鑱婂ぉ鍐呭</span>
+            <Icon
+              icon="ant-design:right-outlined"
+              :size="11"
+              class="text-[var(--ant-color-text-placeholder)]"
+            />
+          </div>
+        </div>
+
+        <div class="flex-shrink-0 h-[10px]"></div>
+
+        <!-- 寮�鍏抽」 -->
+        <div class="bg-[var(--ant-color-bg-container)]">
+          <div class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
+            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">娑堟伅鍏嶆墦鎵�</span>
+            <Switch :checked="!!conversation?.silent" @change="handleMutedChange" />
+          </div>
+          <div class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
+            <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">缃《鑱婂ぉ</span>
+            <Switch :checked="!!conversation?.top" @change="handleTopChange" />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 瀛愬璇濇锛氬彂璧风兢鑱婏紙閿佸畾瀵规柟涓哄凡閫夛級 -->
+    <GroupCreateDialog ref="createGroupDialogRef" @created="handleGroupCreated" />
+  </Drawer>
+</template>
+
+<style scoped>
+/* 銆�+銆� tile锛� hover 鏃惰仈鍔ㄥ唴閮� icon-tile 璧颁富鑹诧紱 璺ㄥ瓙鍏冪礌鐨� hover 鑱斿姩鏃犳硶鐢ㄥ崟鍏冪礌宸ュ叿绫昏〃杈� */
+.im-conversation-private-side__tile-wrap-clickable:hover .im-conversation-private-side__icon-tile {
+  color: var(--ant-color-primary);
+  border-color: var(--ant-color-primary);
+  background-color: var(--ant-color-primary-bg);
+}
+
+/* :deep 绌块�� Icon 鍐呴儴 svg锛� el-icon 鍏ㄥ眬 color 鍦ㄦ殫鑹叉ā寮忎笅琚富棰樼洊杩囷紝閿� fill 鍒板綋鍓嶈壊 */
+.im-conversation-private-side__icon-tile :deep(svg) {
+  fill: currentColor !important;
+}
+
+/* 鐩搁偦淇℃伅琛屽姞鍒嗛殧绾匡紱 鐩搁偦鍏勫紵閫夋嫨鍣ㄦ棤娉曠敤宸ュ叿绫昏〃杈� */
+.im-conversation-private-side__row + .im-conversation-private-side__row {
+  border-top: 1px solid var(--im-border-color-lighter);
+}
+</style>
+
+<!-- 鍚� GroupSide锛歛ntd Drawer 浼犻�佸埌 body 鍚� scoped data-v 钀戒笉鍒� body 涓婏紝闈� root-class-name 浣滅鍏堥�夋嫨鍣ㄥ啓鍏ㄥ眬瑙勫垯鍘嬫帀榛樿 padding -->
+<style>
+.im-conversation-private-side__modal .ant-drawer-body {
+  padding: 0;
+}
+</style>

--
Gitblit v1.9.3