gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
src/views/im/home/components/user/user-info-card.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImConversationType } from '../../../utils/constants'
import { getFriendDisplayName } from '../../../utils/user'
import { useConversationStore } from '../../store/conversationStore'
import { useFriendStore } from '../../store/friendStore'
import { useImUiStore } from '../../store/uiStore'
import UserInfo, { type UserInfoRelation } from './user-info.vue'
defineOptions({ name: 'ImUserInfoCard' })
const uiStore = useImUiStore()
const conversationStore = useConversationStore()
const friendStore = useFriendStore()
const router = useRouter()
const card = computed(() => uiStore.userInfoCard)
const user = computed(() => card.value.user)
const isSelf = computed(() => {
  const myId = getCurrentUserId()
  return !!user.value?.id && user.value.id === myId
})
const isActiveFriend = computed(() => {
  if (!user.value?.id || isSelf.value) {
    return false
  }
  return friendStore.isActiveFriend(user.value.id)
})
const relation = computed<UserInfoRelation>(() => {
  if (!user.value) {
    return 'readonly'
  }
  if (isSelf.value) {
    return 'self'
  }
  if (isActiveFriend.value) {
    return 'friend'
  }
  return 'stranger'
})
const remark = computed(() => {
  if (!isActiveFriend.value || !user.value?.id) {
    return undefined
  }
  return friendStore.getFriend(user.value.id)?.displayName || ''
})
/** å…³é—­åç‰‡ï¼šç‚¹å‡»é®ç½© / Esc / UserInfo æŠ›ä¸Šæ¥çš„删除成功事件 */
function handleClose() {
  uiStore.closeUserInfoCard()
}
/** é”®ç›˜äº‹ä»¶ï¼šEsc é”®å…³é—­åç‰‡ */
function handleKeydown(e: KeyboardEvent) {
  if (e.key === 'Escape' && card.value.show) {
    handleClose()
  }
}
onMounted(() => window.addEventListener('keydown', handleKeydown))
onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
/** å‘消息:UserInfo æŠ›ä¸Šæ¥çš„"发消息",由本组件接力做 openConversation + è·³æ¶ˆæ¯ Tab + å…³æµ®å±‚ */
function handleSendMessage() {
  if (!user.value) {
    return
  }
  // å– friendStore é‡Œçš„æœ€æ–°å¤‡æ³¨ / å…æ‰“扰,避免新建会话用过期数据
  const friend = friendStore.getFriend(user.value.id)
  const conversationName = friend
    ? getFriendDisplayName(friend)
    : user.value.nickname || ''
  conversationStore.openConversation(
    user.value.id,
    ImConversationType.PRIVATE,
    conversationName,
    user.value.avatar || '',
    { silent: !!friend?.silent }
  )
  // è·³è½¬ä¼šè¯ Tab(如果不在的话),并且不管当前路由是啥都先切到会话列表页
  if (router.currentRoute.value.name !== 'ImHomeConversation') {
    router.push({ name: 'ImHomeConversation' })
  }
  uiStore.closeUserInfoCard()
}
</script>
<template>
  <!--
    ç”¨æˆ·åç‰‡æµ®å±‚
    - ä»…承担"浮层定位 + å…³é—­é€»è¾‘(点遮罩 / Esc)",名片视觉走 <UserInfo>,与 contact è¯¦æƒ…共用一份组件
    - è§¦å‘:useImUiStore.openUserInfoCard(user, position);本组件订阅 store,全局只挂一份实例
    - å…³ç³»æ€ç”± isSelf / isActiveFriend æ´¾ç”Ÿ relation prop é€åˆ° UserInfo;删除 / åŠ å¥½å‹ / å¤‡æ³¨è½åº“都在 UserInfo å†…闭环
  -->
  <teleport to="body">
    <div v-if="card.show" class="fixed inset-0 z-9998" @click.self="handleClose">
      <div
        class="fixed w-80 p-4 bg-[var(--ant-color-bg-elevated)] rounded-md shadow-xl"
        :style="{ left: `${card.position.x }px`, top: `${card.position.y }px` }"
        @click.stop
      >
        <UserInfo
          :user="user"
          :display-name="remark"
          :relation="relation"
          :preview-z-index="10000"
          :add-source="card.addSource"
          :add-source-extra="card.addSourceExtra"
          @chat="handleSendMessage"
          @deleted="handleClose"
        />
      </div>
    </div>
  </teleport>
</template>