| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |