liu
5 天以前 f11bd0d8d9c1190340a445a67df9e43cdbde0a3e
src/views/im/home/pages/contact/friend-request-detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,229 @@
<script lang="ts" setup>
import type { FriendRequest, User } from '../../types'
import { computed, ref } from 'vue'
import { prompt } from '#/packages/effects/common-ui/src'
import { DICT_TYPE } from '#/packages/constants/src'
import { getDictLabel } from '#/packages/effects/hooks/src'
import { Button, Input, message } from 'ant-design-vue'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImFriendRequestHandleResult } from '../../../utils/constants'
import { UserAvatar } from '../../components/user'
import { UserInfo } from '../../components/user'
import { useFriendStore } from '../../store/friendStore'
defineOptions({ name: 'ImContactFriendRequestDetail' })
const props = defineProps<{
  request: FriendRequest
}>()
const emit = defineEmits<{
  chat: [peerUserId: number]
}>()
const friendStore = useFriendStore()
/** å½“前登录用户编号;用 computed åŒ…一层,切账号后随 wsCache é‡å–,避免顶层求值在 keep-alive å®žä¾‹é‡ŒæŒæœ‰æ—§ id */
const currentUserId = computed(() => getCurrentUserId())
/** æ˜¯ä¸æ˜¯æˆ‘发起的(fromUserId === currentUserId) */
const iSentIt = computed(() => props.request.fromUserId === currentUserId.value)
/** æ˜¯å¦ã€Œå·²æ‹’绝」态:模板里多处用到,computed ä¸€æ¬¡çœå¾—到处写枚举比对 */
const refused = computed(
  () => props.request.handleResult === ImFriendRequestHandleResult.REFUSED
)
/** æ˜¯å¦ã€Œå·²é€šè¿‡ã€æ€ï¼šè½¬èµ° UserInfo å¥½å‹è¯¦æƒ…入口 */
const agreed = computed(
  () => props.request.handleResult === ImFriendRequestHandleResult.AGREED
)
/** å¯¹ç«¯çš„用户编号 / æ˜µç§° / å¤´åƒ */
const peerUserId = computed(() =>
  iSentIt.value ? props.request.toUserId : props.request.fromUserId
)
const peerNickname = computed(() =>
  iSentIt.value
    ? props.request.toNickname || String(props.request.toUserId)
    : props.request.fromNickname || String(props.request.fromUserId)
)
const peerAvatar = computed(() =>
  iSentIt.value ? props.request.toAvatar : props.request.fromAvatar
)
/** é€ç»™ UserInfo çš„æœ€å°ç”¨æˆ·ä¿¡æ¯ï¼›UserInfo å†…部会按 id è°ƒ getSimpleUser è¡¥é½æ€§åˆ« / éƒ¨é—¨ */
const peerUser = computed<User>(() => ({
  id: peerUserId.value,
  nickname: peerNickname.value,
  avatar: peerAvatar.value
}))
// å„自的 loading ç”¨äºŽæŒ‰é’® spinner æ˜¾ç¤ºï¼›processing æ˜¯è·¨æŒ‰é’®äº’斥锁,避免同意 / æ‹’绝并发提交同一申请
const agreeing = ref(false)
const refusing = ref(false)
const processing = ref(false)
/** åŒæ„ç”³è¯·ï¼šäº’斥锁 + çŠ¶æ€äºŒæ¬¡æ ¡éªŒï¼Œé¿å…å¹¶å‘ / æœåŠ¡ç«¯å·²å¤„ç†åŽå†æ¬¡æäº¤ */
async function handleAgree() {
  if (processing.value) {
    return
  }
  if (props.request.handleResult !== ImFriendRequestHandleResult.UNHANDLED) {
    return
  }
  processing.value = true
  agreeing.value = true
  try {
    await friendStore.agreeFriendRequest(props.request.id)
    message.success('已同意好友申请')
  } finally {
    agreeing.value = false
    processing.value = false
  }
}
/** æ‹’绝申请:弹 prompt æ”¶é›†å¯é€‰æ‹’绝理由(点取消则中止),随后调 store è½åº“ + æç¤º */
async function handleRefuse() {
  if (processing.value) {
    return
  }
  if (props.request.handleResult !== ImFriendRequestHandleResult.UNHANDLED) {
    return
  }
  // 1. å¼¹ prompt æ”¶é›†æ‹’绝理由(最多 255 å­—);用户点「取消」会 reject,中止后续流程
  let handleContent: string | undefined
  try {
    const result = await prompt<string>({
      beforeClose(scope) {
        if (!scope.isConfirm) {
          return
        }
        if ((scope.value || '').length > 255) {
          message.error('最多 255 ä¸ªå­—符')
          return false
        }
      },
      cancelText: '取消',
      component: Input.TextArea,
      componentProps: {
        allowClear: true,
        maxlength: 255,
        placeholder: '不填则不告知对方原因',
        rows: 3
      },
      content: '',
      defaultValue: '',
      confirmText: '拒绝',
      modelPropName: 'value',
      title: '拒绝好友申请'
    })
    handleContent = result || undefined
  } catch {
    return
  }
  // 2. prompt æœŸé—´çŠ¶æ€å¯èƒ½è¢«è·¨ç«¯æ”¹æˆ AGREED / REFUSED,再校验一次避免重复提交
  if (processing.value) {
    return
  }
  if (props.request.handleResult !== ImFriendRequestHandleResult.UNHANDLED) {
    return
  }
  processing.value = true
  refusing.value = true
  try {
    await friendStore.refuseFriendRequest(props.request.id, handleContent)
    message.success('已拒绝好友申请')
  } finally {
    refusing.value = false
    processing.value = false
  }
}
</script>
<template>
  <!--
    æ–°çš„æœ‹å‹è¯¦æƒ…面板
    - å·²é€šè¿‡ â†’ ç›´æŽ¥èµ° UserInfo å¥½å‹è¯¦æƒ…,跟通讯录里点开好友的体验完全一致
    - æœªå¤„理 / å·²æ‹’绝 â†’ ç”³è¯·æ€é¢æ¿ï¼šå¤´åƒ + ç”³è¯·/拒绝对话气泡 + æ¥æº + æ“ä½œæŒ‰é’®
  -->
  <div v-if="agreed" class="flex justify-center pt-12 px-6">
    <div class="w-full max-w-[320px]">
      <UserInfo
        :user="peerUser"
        :display-name="friendStore.getFriend(peerUserId)?.displayName || ''"
        relation="friend"
        @chat="emit('chat', peerUserId)"
      />
    </div>
  </div>
  <div v-else class="flex flex-col items-center px-6 pt-12">
    <UserAvatar
      :id="peerUserId"
      :url="peerAvatar"
      :name="peerNickname"
      :size="64"
      :clickable="false"
    />
    <div class="mt-3 text-base font-semibold text-[var(--ant-color-text)]">
      {{ peerNickname }}
    </div>
    <!-- ç”³è¯·ç†ç”±å—:仿微信灰底气泡,按申请方身份前缀;长文本 break-words æŠ˜è¡Œ -->
    <div
      v-if="request.applyContent"
      class="w-full max-w-[420px] mt-6 px-3.5 py-3 rounded-md bg-[var(--ant-color-fill-secondary)] text-13px text-[var(--ant-color-text)] break-words"
    >
      <span class="text-[var(--ant-color-text-secondary)]">
        {{ iSentIt ? '我' : peerNickname }}:
      </span>
      <span class="ml-1">{{ request.applyContent }}</span>
    </div>
    <!-- æ¥æºè¡Œï¼šlabel å·¦ã€value å³ï¼Œå¯¹é½å¾®ä¿¡ã€Œæ¥æºã€è¡Œ -->
    <div
      v-if="request.addSource"
      class="w-full max-w-[420px] mt-3 flex items-center text-13px text-[var(--ant-color-text-secondary)]"
    >
      <span class="w-16 flex-shrink-0 whitespace-nowrap">来源</span>
      <span class="text-[var(--ant-color-text)]">
        {{ getDictLabel(DICT_TYPE.IM_FRIEND_ADD_SOURCE, request.addSource) }}
      </span>
    </div>
    <!-- æ‹’绝理由:label å·¦ã€value å³ï¼›é•¿æ–‡æœ¬ break-words è‡ªåŠ¨æŠ˜è¡Œ -->
    <div
      v-if="refused && request.handleContent"
      class="w-full max-w-[420px] mt-3 flex items-start text-13px text-[var(--ant-color-text-secondary)]"
    >
      <span class="w-16 flex-shrink-0 whitespace-nowrap">拒绝理由</span>
      <span class="flex-1 min-w-0 break-words text-[var(--ant-color-text)]">
        {{ request.handleContent }}
      </span>
    </div>
    <!-- æ“ä½œæŒ‰é’® -->
    <div class="w-full max-w-[420px] mt-8 flex justify-center">
      <!-- æˆ‘发起 + ç­‰å¾…中:禁用「等待对方验证」 -->
      <Button
        v-if="iSentIt && request.handleResult === ImFriendRequestHandleResult.UNHANDLED"
        disabled
      >
        ç­‰å¾…对方验证
      </Button>
      <!-- åˆ«äººåŠ æˆ‘ + ç­‰å¾…中:同意 / æ‹’绝 -->
      <template v-if="!iSentIt && request.handleResult === ImFriendRequestHandleResult.UNHANDLED">
        <Button @click="handleRefuse" :loading="refusing" :disabled="processing">拒绝</Button>
        <Button type="primary" @click="handleAgree" :loading="agreeing" :disabled="processing">
          åŒæ„
        </Button>
      </template>
      <!-- å·²æ‹’绝:占位禁用按钮 -->
      <Button v-if="refused" disabled>已拒绝</Button>
    </div>
  </div>
</template>