gaoluyang
2 天以前 b64a0deae5b5d33f9e20671a68936b27f0b9b00b
src/views/im/home/components/group/group-request-list-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,377 @@
<script lang="ts" setup>
import type { ImGroupRequestApi } from '#/api/im/group/request'
import { computed, ref, watch } from 'vue'
import { prompt } from '#/packages/effects/common-ui/src'
import { Empty, Input, message, Modal, Spin } from 'ant-design-vue'
import { getGroupRequestListByGroupId } from '#/api/im/group/request'
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
import { useGroupRequestStore } from '../../store/groupRequestStore'
import { UserAvatar } from '../user'
defineOptions({ name: 'ImGroupRequestListDialog' })
const groupRequestStore = useGroupRequestStore()
const visible = ref(false)
const groupId = ref<number | undefined>() // å½“前展示的群编号;undefined æ—¶èµ°å…¨å±€æœªå¤„理列表(store.unhandledList)
const loading = ref(false)
const groupList = ref<ImGroupRequestApi.GroupRequestRespVO[]>([])
const actingId = ref<null | number>(null)
defineExpose({
  /** æ‰“开进群申请弹窗:reset â†’ çŒå‚ â†’ visible=true;不传 groupId èµ°å…¨å±€æœªå¤„理列表 */
  open(opts?: { groupId?: number }) {
    groupId.value = opts?.groupId
    actingId.value = null
    visible.value = true
  }
})
/** æ•°æ®æºï¼šå•群模式用 fetch å›žæ¥çš„ groupList;全局模式直接读 store.unhandledList,处理后 store è‡ªåЍ reactive åŒæ­¥ */
const list = computed<ImGroupRequestApi.GroupRequestRespVO[]>(() =>
  groupId.value ? groupList.value : groupRequestStore.unhandledList
)
/** é¡¶éƒ¨å¡ç‰‡ï¼šæœ€æ–°ä¸€æ¡ï¼›ç©ºæ•°ç»„时为 null */
const latest = computed(() => list.value[0] || null)
/** åŽ†å²åˆ—è¡¨ï¼šé™¤æœ€æ–°ä¸€æ¡å¤–çš„å…¶ä½™ */
const histories = computed(() => list.value.slice(1))
/** æ‰“å¼€ dialog æ—¶æ‹‰æ•°æ®ï¼šå•群拉 API;全局直接读 store;关闭时清掉单群缓存 */
watch(
  [visible, groupId],
  ([isVisible, currentGroupId]) => {
    if (isVisible && currentGroupId) {
      void fetchList(currentGroupId)
    } else if (!isVisible) {
      groupList.value = []
    }
  },
  { immediate: true }
)
/**
 * å•群模式下订阅 store ä¸­å½’属本群的未处理列表变化:远端事件(WS 1503 æ–°ç”³è¯· / å…¶ä»–管理员处理)触发时 refetch
 * æ‹¿æœ€æ–° handleResult;本端 agree / refuse æœŸé—´ actingId é”ä½ï¼Œè·³è¿‡æœ¬ç«¯åŠ¨ä½œå¼•å‘çš„ store å˜åŒ–避免冗余 RTT
 *
 * key ä¸èƒ½åª join id:复用旧记录时同一 requestId çš„ applyContent / inviterUserId ä¼šåˆ·æ–°ä½† id ä¸å˜ï¼Œå¿…须把内容字段也纳入触发
 */
watch(
  () =>
    groupId.value && visible.value
      ? groupRequestStore.unhandledList
          .filter((request) => request.groupId === groupId.value)
          .map(
            (request) =>
              `${request.id}:${request.inviterUserId ?? ''}:${request.applyContent ?? ''}`
          )
          .join(',')
      : null,
  (current, previous) => {
    if (current === null || previous === undefined || current === previous) {
      return
    }
    if (actingId.value !== null) {
      return
    }
    if (groupId.value) {
      void fetchList(groupId.value)
    }
  }
)
let fetchSeq = 0 // å•调递增请求序号;同群也会因为 WS 1503 æŽ¨é€è§¦å‘额外 fetch,乱序返回时旧响应不能覆盖新数据
async function fetchList(targetGroupId: number) {
  const seq = ++fetchSeq
  loading.value = true
  try {
    const data = (await getGroupRequestListByGroupId(targetGroupId)) || []
    // æœŸé—´åˆ‡ç¾¤ / å…³å¼¹çª— / åˆè§¦å‘æ›´æ–° fetch:丢响应
    if (seq !== fetchSeq || !visible.value || groupId.value !== targetGroupId) {
      return
    }
    groupList.value = data
  } finally {
    // æ—§è¯·æ±‚ finally å‘½ä¸­æ—¶æ–°è¯·æ±‚仍在跑,跳过避免提前关 loading
    if (seq === fetchSeq) {
      loading.value = false
    }
  }
}
/** åŒæ„ï¼šèµ° store åŒæ­¥å…¨å±€æœªå¤„理列表 + æœ¬åœ°æ›´æ–° handleResult è®©æŒ‰é’®å˜ç° */
async function handleAgree(item: ImGroupRequestApi.GroupRequestRespVO) {
  if (actingId.value !== null) return
  actingId.value = item.id
  try {
    await groupRequestStore.agreeGroupRequest(item.id)
    updateLocalResult(item.id, ImGroupRequestHandleResult.AGREED)
    message.success('已同意')
  } finally {
    actingId.value = null
  }
}
/** æ‹’绝:弹理由输入框;为空则不带 handleContent */
async function handleRefuse(item: ImGroupRequestApi.GroupRequestRespVO) {
  if (actingId.value !== null) return
  let handleContent: string
  try {
    const result = await prompt<string>({
      component: Input,
      componentProps: {
        allowClear: true,
        placeholder: '请输入拒绝理由(可选)'
      },
      content: '',
      modelPropName: 'value',
      title: '拒绝申请'
    })
    handleContent = result || ''
  } catch {
    return
  }
  actingId.value = item.id
  try {
    await groupRequestStore.refuseGroupRequest(item.id, handleContent || undefined)
    updateLocalResult(item.id, ImGroupRequestHandleResult.REFUSED)
    message.success('已拒绝')
  } finally {
    actingId.value = null
  }
}
/** å•群模式下处理后更新 groupList é‡Œçš„ handleResult,按钮转「已同意 / å·²æ‹’绝」灰态;全局模式 store ç›´æŽ¥ç§»é™¤è¯¥é¡¹æ— éœ€æ›´æ–° */
function updateLocalResult(id: number, handleResult: number) {
  const target = groupList.value.find((r) => r.id === id)
  if (target) {
    target.handleResult = handleResult
  }
}
</script>
<template>
  <!--
    ç¾¤ã€Œè¿›ç¾¤ç”³è¯·ã€åˆ—表对话框
    - ä»…群主 / ç®¡ç†å‘˜å…¥å£å¯è¾¾ï¼›å±•示当前群下全部申请(含已处理)
    - é¡¶éƒ¨æœ€æ–°ä¸€æ¡å¡ç‰‡åŒ–突出(带申请理由),其余按 id å€’序紧凑列表
    - åŒæ„ / æ‹’绝走 groupRequestStore çš„ action,处理后本地更新 handleResult è®©æŒ‰é’®è½¬ç°æ€
  -->
  <Modal
    v-model:open="visible"
    title="进群申请"
    width="560px"
    :footer="null"
    :mask-closable="false"
    class="im-group-request-list__dialog"
  >
    <Spin :spinning="loading" wrapper-class-name="w-full">
      <div class="flex flex-col gap-3 max-h-[60vh] overflow-y-auto pr-1">
        <!-- ç©ºæ€ -->
        <Empty v-if="!loading && list.length === 0" description="暂无进群申请" />
      <!-- é¡¶éƒ¨å¡ç‰‡ï¼šæœ€æ–°ä¸€æ¡ -->
        <div
          v-if="latest"
          class="flex flex-col gap-2.5 p-3.5 rounded-[10px] border border-solid border-[var(--ant-color-border-secondary)] bg-[var(--ant-color-bg-container)] shadow-[0_1px_3px_rgba(0,0,0,0.04)]"
        >
        <div class="flex items-center gap-3">
          <UserAvatar
            :url="latest.userAvatar"
            :name="latest.userNickname"
            :size="44"
            :clickable="false"
          />
          <div class="flex-1 min-w-0">
            <div
              class="truncate text-sm font-medium leading-[1.4] text-[var(--ant-color-text)]"
            >
              {{ latest.userNickname || `用户 ${latest.userId}` }}
            </div>
            <div
              class="truncate mt-[2px] text-12px leading-[1.5] text-[var(--ant-color-text-secondary)]"
            >
              <template v-if="latest.inviterUserId">
                é€šè¿‡
                <span class="text-[var(--ant-color-primary)]">
                  {{ latest.inviterNickname || `用户 ${latest.inviterUserId}` }}
                </span>
                çš„邀请进群
              </template>
              <template v-else>申请加入</template>
            </div>
          </div>
          <span
            v-if="latest.handleResult === ImGroupRequestHandleResult.AGREED"
            class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
          >
            å·²åŒæ„
          </span>
          <span
            v-else-if="latest.handleResult === ImGroupRequestHandleResult.REFUSED"
            class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
          >
            å·²æ‹’绝
          </span>
          <div v-else class="flex gap-1.5 flex-shrink-0">
            <button
              class="im-group-request-list__btn im-group-request-list__btn--primary"
              :disabled="actingId === latest.id"
              @click="handleAgree(latest)"
            >
              ç¡®è®¤
            </button>
            <button
              class="im-group-request-list__btn im-group-request-list__btn--ghost"
              :disabled="actingId === latest.id"
              @click="handleRefuse(latest)"
            >
              æ‹’绝
            </button>
          </div>
        </div>
        <!-- ç”³è¯·ç†ç”±ï¼šé‚€è¯·åœºæ™¯æ˜¾ç¤ºé‚€è¯·äºº + ç•™è¨€ï¼›ä¸»åŠ¨ç”³è¯·æ˜¾ç¤ºç”³è¯·äºº + ç•™è¨€ -->
        <div
          v-if="latest.applyContent"
          class="px-3 py-2 rounded-md text-[13px] leading-[1.5] break-all bg-[var(--ant-color-fill-secondary)] text-[var(--ant-color-text)]"
        >
          <span class="text-[var(--ant-color-primary)]">
            {{
              latest.inviterUserId
                ? latest.inviterNickname || `用户 ${latest.inviterUserId}`
                : latest.userNickname || `用户 ${latest.userId}`
            }}:
          </span>
          {{ latest.applyContent }}
        </div>
      </div>
      <!-- åˆ†å‰²çº¿ï¼šä»…在有更早申请时出现 -->
        <div
          v-if="histories.length > 0"
          class="flex items-center justify-center mt-1.5 -mb-0.5 text-12px text-[var(--ant-color-text-placeholder)]"
        >
          <span>以下为更早的申请</span>
        </div>
        <!-- åŽ†å²ç”³è¯·åˆ—è¡¨ -->
        <div
          v-for="item in histories"
          :key="item.id"
          class="flex flex-col gap-2.5 px-3.5 py-2.5 rounded-[10px] border border-solid border-[var(--ant-color-border-secondary)] bg-[var(--ant-color-bg-container)] shadow-[0_1px_3px_rgba(0,0,0,0.04)]"
        >
        <div class="flex items-center gap-3">
          <UserAvatar
            :url="item.userAvatar"
            :name="item.userNickname"
            :size="40"
            :clickable="false"
          />
          <div class="flex-1 min-w-0">
            <div
              class="truncate text-sm font-medium leading-[1.4] text-[var(--ant-color-text)]"
            >
              {{ item.userNickname || `用户 ${item.userId}` }}
            </div>
            <div
              class="truncate mt-[2px] text-12px leading-[1.5] text-[var(--ant-color-text-secondary)]"
            >
              <template v-if="item.inviterUserId">
                é€šè¿‡
                <span class="text-[var(--ant-color-primary)]">
                  {{ item.inviterNickname || `用户 ${item.inviterUserId}` }}
                </span>
                çš„邀请进群
              </template>
              <template v-else>申请加入</template>
            </div>
          </div>
          <span
            v-if="item.handleResult === ImGroupRequestHandleResult.AGREED"
            class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
          >
            å·²åŒæ„
          </span>
          <span
            v-else-if="item.handleResult === ImGroupRequestHandleResult.REFUSED"
            class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
          >
            å·²æ‹’绝
          </span>
          <div v-else class="flex gap-1.5 flex-shrink-0">
            <button
              class="im-group-request-list__btn im-group-request-list__btn--primary"
              :disabled="actingId === item.id"
              @click="handleAgree(item)"
            >
              ç¡®è®¤
            </button>
            <button
              class="im-group-request-list__btn im-group-request-list__btn--ghost"
              :disabled="actingId === item.id"
              @click="handleRefuse(item)"
            >
              æ‹’绝
            </button>
          </div>
        </div>
        </div>
      </div>
    </Spin>
  </Modal>
</template>
<style scoped>
/* è‡ªç»˜æŒ‰é’®ï¼šè´´è¿‘微信小药丸样式;与 :disabled、:hover:not(:disabled) ç­‰ä¼ªç±»å åŠ  modifier ç±»çš„组合选择器写在 class é‡Œæˆæœ¬é«˜ï¼Œç•™ SCSS */
.im-group-request-list__btn {
  flex-shrink: 0;
  min-width: 56px;
  height: 28px;
  padding: 0 12px;
  font-size: 13px;
  border-radius: 4px;
  cursor: pointer;
  border: 1px solid transparent;
  transition:
    background-color 0.15s,
    border-color 0.15s,
    color 0.15s;
}
.im-group-request-list__btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.im-group-request-list__btn--primary {
  color: #fff;
  background-color: var(--ant-color-primary);
  border-color: var(--ant-color-primary);
}
.im-group-request-list__btn--primary:hover:not(:disabled) {
  background-color: var(--ant-color-primary-hover);
  border-color: var(--ant-color-primary-hover);
}
.im-group-request-list__btn--ghost {
  color: var(--ant-color-text);
  background-color: var(--ant-color-bg-container);
  border-color: var(--ant-color-border);
}
.im-group-request-list__btn--ghost:hover:not(:disabled) {
  color: var(--ant-color-primary);
  border-color: var(--ant-color-primary);
}
</style>
<style>
/* el-dialog å†…部 body é€šè¿‡ teleport æ¸²æŸ“到 body,scoped é€‰ä¸åˆ°ï¼Œç•™éž scoped å…¨å±€è¦†ç›– */
.im-group-request-list__dialog .el-dialog__body {
  padding: 12px 20px 8px;
  background-color: var(--ant-color-fill-secondary);
}
</style>