feat(im): 初始化群申请 v0.3:第四把 review(优化界面,进一步对齐微信界面)

im
YunaiV 2026-05-06 23:57:03 +08:00
parent 0eca952c6a
commit f746aebe08
6 changed files with 473 additions and 185 deletions

View File

@ -47,19 +47,6 @@ export const refuseGroupRequest = (id: number | string, handleContent?: string)
})
}
// 查询「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页
// TODO @AI这个 list 接口,改成传递 groupId查询这个群下所有的申请。然后group size 增加一个:「群申请列表」,里面可以看到所有的。
export const getMyGroupRequestList = (limit: number, lastRequestId?: number) => {
const params: Record<string, number> = { limit }
if (lastRequestId != null) {
params.lastRequestId = lastRequestId
}
return request.get<ImGroupRequestRespVO[]>({
url: '/im/group-request/list',
params
})
}
// 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表
export const getUnhandledRequestList = () => {
return request.get<ImGroupRequestRespVO[]>({
@ -67,6 +54,14 @@ export const getUnhandledRequestList = () => {
})
}
// 查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查
export const getGroupRequestListByGroupId = (groupId: number) => {
return request.get<ImGroupRequestRespVO[]>({
url: '/im/group-request/list-by-group',
params: { groupId }
})
}
// 按 id 单查申请记录带越权过滤WebSocket 通知到达后用)
export const getMyGroupRequest = (id: number) => {
return request.get<ImGroupRequestRespVO | null>({

View File

@ -0,0 +1,391 @@
<template>
<!--
进群申请列表对话框
- 仅群主 / 管理员入口可达展示当前群下全部申请含已处理
- 顶部最新一条卡片化突出带申请理由其余按 id 倒序紧凑列表
- 同意 / 拒绝走 groupRequestStore action处理后本地更新 handleResult 让按钮转灰态
-->
<el-dialog
v-model="visible"
title="进群申请"
width="560px"
:close-on-click-modal="false"
class="im-group-request-list__dialog"
>
<div v-loading="loading" class="im-group-request-list__body">
<!-- 空态 -->
<el-empty v-if="!loading && list.length === 0" description="暂无进群申请" />
<!-- 顶部卡片最新一条 -->
<div v-if="latest" class="im-group-request-list__card">
<div class="im-group-request-list__row">
<UserAvatar
:url="latest.userAvatar"
:name="latest.userNickname"
:size="44"
:clickable="false"
/>
<div class="im-group-request-list__main">
<div class="im-group-request-list__name truncate">
{{ latest.userNickname || `用户 ${latest.userId}` }}
</div>
<div class="im-group-request-list__source truncate">
<template v-if="latest.inviterUserId">
通过
<span class="im-group-request-list__inviter">
{{ latest.inviterNickname || `用户 ${latest.inviterUserId}` }}
</span>
的邀请进群
</template>
<template v-else></template>
</div>
</div>
<span
v-if="latest.handleResult === ImGroupRequestHandleResult.AGREED"
class="im-group-request-list__done"
>
已同意
</span>
<span
v-else-if="latest.handleResult === ImGroupRequestHandleResult.REFUSED"
class="im-group-request-list__done"
>
已拒绝
</span>
<div v-else class="im-group-request-list__actions">
<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="im-group-request-list__quote">
<span class="im-group-request-list__quote-name">
{{
latest.inviterUserId
? latest.inviterNickname || `用户 ${latest.inviterUserId}`
: latest.userNickname || `用户 ${latest.userId}`
}}
</span>
{{ latest.applyContent }}
</div>
</div>
<!-- 分割线仅在有更早申请时出现 -->
<div v-if="histories.length > 0" class="im-group-request-list__divider">
<span>以下为更早的申请</span>
</div>
<!-- 历史申请列表 -->
<div
v-for="item in histories"
:key="item.id"
class="im-group-request-list__card im-group-request-list__card--compact"
>
<div class="im-group-request-list__row">
<UserAvatar
:url="item.userAvatar"
:name="item.userNickname"
:size="40"
:clickable="false"
/>
<div class="im-group-request-list__main">
<div class="im-group-request-list__name truncate">
{{ item.userNickname || `用户 ${item.userId}` }}
</div>
<div class="im-group-request-list__source truncate">
<template v-if="item.inviterUserId">
通过
<span class="im-group-request-list__inviter">
{{ item.inviterNickname || `用户 ${item.inviterUserId}` }}
</span>
的邀请进群
</template>
<template v-else></template>
</div>
</div>
<span
v-if="item.handleResult === ImGroupRequestHandleResult.AGREED"
class="im-group-request-list__done"
>
已同意
</span>
<span
v-else-if="item.handleResult === ImGroupRequestHandleResult.REFUSED"
class="im-group-request-list__done"
>
已拒绝
</span>
<div v-else class="im-group-request-list__actions">
<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>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import { getGroupRequestListByGroupId, type ImGroupRequestRespVO } from '@/api/im/group/request'
import { ImGroupRequestHandleResult } from '@/views/im/utils/constants'
import { useGroupRequestStore } from '../../store/groupRequestStore'
import UserAvatar from '../user/UserAvatar.vue'
defineOptions({ name: 'ImGroupRequestListDialog' })
const props = defineProps<{
modelValue: boolean
groupId?: number
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const message = useMessage()
const groupRequestStore = useGroupRequestStore()
const loading = ref(false)
const groupList = ref<ImGroupRequestRespVO[]>([])
const actingId = ref<number | null>(null)
/** 数据源:单群模式用 fetch 回来的 groupList全局模式直接读 store.unhandledList处理后 store 自动 reactive 同步 */
const list = computed<ImGroupRequestRespVO[]>(() =>
props.groupId ? groupList.value : groupRequestStore.unhandledList
)
/** 顶部卡片:最新一条;空数组时为 null */
const latest = computed(() => list.value[0] || null)
/** 历史列表:除最新一条外的其余 */
const histories = computed(() => list.value.slice(1))
/** 打开 dialog 时拉数据:单群拉 API全局直接读 store关闭时清掉单群缓存 */
watch(
() => [visible.value, props.groupId] as const,
([open, groupId]) => {
if (open && groupId) {
void fetchList(groupId)
} else if (!open) {
groupList.value = []
}
},
{ immediate: true }
)
async function fetchList(groupId: number) {
loading.value = true
try {
groupList.value = (await getGroupRequestListByGroupId(groupId)) || []
} finally {
loading.value = false
}
}
/** 同意:走 store 同步全局未处理列表 + 本地更新 handleResult 让按钮变灰 */
async function handleAgree(item: ImGroupRequestRespVO) {
if (actingId.value !== null) return
actingId.value = item.id
try {
await groupRequestStore.agreeRequest(item.id)
updateLocalResult(item.id, ImGroupRequestHandleResult.AGREED)
message.success('已同意')
} finally {
actingId.value = null
}
}
/** 拒绝:弹理由输入框;为空则不带 handleContent */
async function handleRefuse(item: ImGroupRequestRespVO) {
if (actingId.value !== null) return
let handleContent = ''
try {
const result = await message.prompt('请输入拒绝理由(可选)', '拒绝申请')
handleContent = result.value || ''
} catch {
return
}
actingId.value = item.id
try {
await groupRequestStore.refuseRequest(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>
<style scoped>
.im-group-request-list__body {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 60vh;
overflow-y: auto;
padding-right: 4px;
}
.im-group-request-list__card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px;
background-color: var(--el-bg-color);
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
border: 1px solid var(--el-border-color-lighter);
}
.im-group-request-list__card--compact {
padding: 10px 14px;
}
.im-group-request-list__row {
display: flex;
align-items: center;
gap: 12px;
}
.im-group-request-list__main {
flex: 1;
min-width: 0;
}
.im-group-request-list__name {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
line-height: 1.4;
}
.im-group-request-list__source {
margin-top: 2px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.5;
}
.im-group-request-list__inviter {
color: var(--el-color-primary);
}
.im-group-request-list__quote {
padding: 8px 12px;
background-color: var(--el-fill-color-light);
border-radius: 6px;
font-size: 13px;
color: var(--el-text-color-regular);
line-height: 1.5;
word-break: break-all;
}
.im-group-request-list__quote-name {
color: var(--el-color-primary);
}
.im-group-request-list__divider {
display: flex;
align-items: center;
justify-content: center;
margin: 6px 0 -2px;
font-size: 12px;
color: var(--el-text-color-placeholder);
}
.im-group-request-list__actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
/* 自绘按钮贴近微信小药丸样式el-button 默认尺寸偏大、圆角偏方 */
.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(--el-color-primary);
border-color: var(--el-color-primary);
}
.im-group-request-list__btn--primary:hover:not(:disabled) {
background-color: var(--el-color-primary-light-3);
border-color: var(--el-color-primary-light-3);
}
.im-group-request-list__btn--ghost {
color: var(--el-text-color-regular);
background-color: var(--el-bg-color);
border-color: var(--el-border-color);
}
.im-group-request-list__btn--ghost:hover:not(:disabled) {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
.im-group-request-list__done {
flex-shrink: 0;
font-size: 13px;
color: var(--el-text-color-placeholder);
}
</style>
<style>
.im-group-request-list__dialog .el-dialog__body {
padding: 12px 20px 8px;
background-color: var(--el-fill-color-light);
}
</style>

View File

@ -57,6 +57,13 @@ const { readActive, syncPrivateReadStatus } = useMessageSender()
/** 初始化:先吃本地缓存让首屏立即渲染,再远端刷新最新数据,最后建实时通信拉离线消息 */
onMounted(async () => {
// 0.1 IDB /
void faceStore.ensureFacePacks().catch((e) => console.warn('[IM] 后台预拉表情包失败', e))
// 0.2 / store
void groupRequestStore.fetchUnhandledList().catch((e) =>
console.warn('[IM] 拉取未处理加群申请失败', e)
)
// 1.1 loading=true saveConversations + WebSocket connect pullOnce maxId pull 线
conversationStore.loading = true
try {

View File

@ -272,13 +272,34 @@
<span class="im-conversation-group-side__label">全群禁言</span>
<el-switch :model-value="!!currentMutedAll" @change="onMuteAllChange" />
</div>
<!-- 进群审批仅群主可操作开启后所有申请邀请路径都需群主 / 管理员同意 -->
<div v-if="isOwner" class="im-conversation-group-side__row">
<span class="im-conversation-group-side__label">进群需要群主 / 群管理确认</span>
<el-switch :model-value="!!group.joinApproval" @change="onJoinApprovalChange" />
</div>
</div>
<!-- ==================== 进群审批 ==================== -->
<!-- 单独一段群主开关 + 紧跟- 进群申请子项与微信群管理布局对齐 -->
<template v-if="isOwner || (isOwnerOrAdmin && !!group.joinApproval)">
<div class="im-conversation-group-side__spacer"></div>
<div class="im-conversation-group-side__section">
<!-- 进群审批仅群主可操作开启后普通成员的申请邀请路径都需群主 / 管理员同意群主 / 管理员邀请直进 -->
<div v-if="isOwner" class="im-conversation-group-side__row">
<span class="im-conversation-group-side__label">进群需要群主 / 群管理确认</span>
<el-switch :model-value="!!group.joinApproval" @change="onJoinApprovalChange" />
</div>
<!-- 进群申请子项仅当开启审批 + 当前用户是 owner / admin 时出现点击进列表 dialog -->
<div
v-if="isOwnerOrAdmin && !!group.joinApproval"
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
@click="requestListVisible = true"
>
<span class="im-conversation-group-side__label">- 进群申请</span>
<Icon
icon="ant-design:right-outlined"
:size="11"
class="im-conversation-group-side__chevron"
/>
</div>
</div>
</template>
<!-- ==================== 群主操作 ==================== -->
<!-- 仅群主可见含管理员设置 + 群主管理权转让 -->
<template v-if="isOwner">
@ -371,6 +392,9 @@
:max-size="1"
@complete="handleTransferOwnerComplete"
/>
<!-- 进群申请列表仅当开启审批 + 当前用户是 owner / admin 时入口可见 -->
<GroupRequestListDialog v-model="requestListVisible" :group-id="group?.id" />
</el-drawer>
</template>
@ -402,6 +426,7 @@ import GroupMemberAddDialog from '../../../../components/group/GroupMemberAddDia
import GroupMemberSelector, {
type GroupMemberFlag
} from '../../../../components/group/GroupMemberSelector.vue'
import GroupRequestListDialog from '../../../../components/group/GroupRequestListDialog.vue'
import type { Conversation, FriendLite, GroupLite } from '../../../../types'
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
@ -445,6 +470,7 @@ const inviteVisible = ref(false)
const removeVisible = ref(false)
const adminVisible = ref(false)
const transferOwnerVisible = ref(false)
const requestListVisible = ref(false)
const showAllMembers = ref(false)
const namePopoverVisible = ref(false)
const noticePopoverVisible = ref(false)

View File

@ -42,6 +42,10 @@
</span>
</div>
<div class="flex items-center mt-1 leading-5">
<!-- 进群申请红字前缀群主 / 管理员看到自己管理的群下还有未处理申请时显示 -->
<span v-if="requestText" class="flex-shrink-0 text-12px text-[#c70b0b]">
{{ requestText }}
</span>
<!-- @红字提示atMe 优先于 atAll -->
<span v-if="atText" class="flex-shrink-0 text-12px text-[#c70b0b]">{{ atText }}</span>
<!-- 群聊最后一条发送者前缀 lastSenderId + 当前会话上下文实时算名字 -->
@ -78,6 +82,7 @@ import { useMessage } from '@/hooks/web/useMessage'
import { useConversationStore } from '../../../../store/conversationStore'
import { useFriendStore } from '../../../../store/friendStore'
import { useGroupStore } from '../../../../store/groupStore'
import { useGroupRequestStore } from '../../../../store/groupRequestStore'
import { useImUiStore } from '../../../../store/uiStore'
import { useDraftStore } from '../../../../store/draftStore'
import { ImConversationType, ImMessageType, isNormalMessage } from '../../../../../utils/constants'
@ -98,6 +103,7 @@ const props = defineProps<{
const conversationStore = useConversationStore()
const friendStore = useFriendStore()
const groupStore = useGroupStore()
const groupRequestStore = useGroupRequestStore()
const uiStore = useImUiStore()
const draftStore = useDraftStore()
const message = useMessage()
@ -176,6 +182,15 @@ const atText = computed(() => {
return ''
})
/** 群聊未处理加群申请红字前缀store 已经按「我管理的群」过滤过count > 0 即可显示 */
const requestText = computed(() => {
if (!isGroup.value) {
return ''
}
const count = groupRequestStore.getUnhandledCountByGroupId(props.conversation.targetId)
return count > 0 ? `[${count}条进群申请]` : ''
})
/** 点击切会话 */
function handleClick() {
conversationStore.setActiveConversation(props.conversation)

View File

@ -2,79 +2,38 @@
<!--
群顶部待处理加群申请横幅
- 仅当登录用户是该群 owner / admin 且该群下未处理申请数 > 0 时显示
- count list groupRequestStore 派生全局存本端处理 / WS 通知到达后 store 自动更新
- 单条胶囊一行点击展开下拉每条带同意 / 拒绝按钮
- count groupRequestStore 派生全局存本端处理 / WS 通知到达后 store 自动更新
- 点击横幅打开 GroupRequestListDialog含历史已处理记录不再就地展开
-->
<!-- TODO @AI还不是新建一个 components/group这个改成 GroupRequestPending然后 GroupPinnedMessages 这样风格更一致一点 -->
<div v-if="canManage && pendingCount > 0" class="im-conversation-group-request">
<div
class="im-conversation-group-request__row im-conversation-group-request__row--clickable"
@click="expanded = !expanded"
>
<div class="im-conversation-group-request__row" @click="dialogVisible = true">
<Icon
icon="ant-design:user-add-outlined"
:size="14"
class="im-conversation-group-request__icon"
/>
<span class="im-conversation-group-request__text">
{{ pendingCount }} 条新的入群申请待处理
</span>
<span class="im-conversation-group-request__text"> 新进群申请{{ pendingCount }} </span>
<Icon
:icon="expanded ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
icon="ant-design:right-outlined"
:size="11"
class="im-conversation-group-request__chevron"
/>
</div>
<!-- 展开列表面板浅色面板 + 每条独立卡片行内含同意 / 拒绝按钮 -->
<div v-if="expanded" class="im-conversation-group-request__list">
<div v-for="item in list" :key="item.id" class="im-conversation-group-request__item">
<UserAvatar
:url="item.userAvatar"
:name="item.userNickname"
:size="32"
:clickable="false"
/>
<div class="im-conversation-group-request__item-body">
<div class="im-conversation-group-request__item-name truncate">
{{ item.userNickname || `用户 ${item.userId}` }}
<span v-if="item.inviterUserId" class="im-conversation-group-request__item-inviter">
{{ item.inviterNickname || `用户 ${item.inviterUserId}` }} 邀请
</span>
</div>
<div v-if="item.applyContent" class="im-conversation-group-request__item-msg truncate">
{{ item.applyContent }}
</div>
</div>
<div class="im-conversation-group-request__item-actions">
<el-button
size="small"
type="primary"
:loading="actingId === item.id"
@click="handleAgree(item)"
>
同意
</el-button>
<el-button size="small" :loading="actingId === item.id" @click="handleRefuse(item)">
拒绝
</el-button>
</div>
</div>
</div>
<!-- 申请列表 dialog复用同一组件避免群管理面板与会话顶部各写一份 -->
<GroupRequestListDialog v-model="dialogVisible" :group-id="groupId" />
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { computed, ref } from 'vue'
import Icon from '@/components/Icon/src/Icon.vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useUserStore } from '@/store/modules/user'
import type { ImGroupRequestRespVO } from '@/api/im/group/request'
import { ImGroupMemberRole } from '@/views/im/utils/constants'
import { useGroupStore } from '../../../../store/groupStore'
import { useGroupRequestStore } from '../../../../store/groupRequestStore'
import UserAvatar from '../../../../components/user/UserAvatar.vue'
import GroupRequestListDialog from '../../../../components/group/GroupRequestListDialog.vue'
defineOptions({ name: 'ImConversationGroupRequestPending' })
@ -85,10 +44,8 @@ const props = defineProps<{
const userStore = useUserStore()
const groupStore = useGroupStore()
const groupRequestStore = useGroupRequestStore()
const message = useMessage()
const expanded = ref(false)
const actingId = ref<number | null>(null)
const dialogVisible = ref(false)
/** 当前群(含 ownerUserId / members */
const group = computed(() => groupStore.getGroup(props.groupId))
@ -109,62 +66,11 @@ const canManage = computed(
/** 当前群未处理申请数;从 store 派生 */
const pendingCount = computed(() => groupRequestStore.getUnhandledCountByGroupId(props.groupId))
/** 当前群未处理申请列表Drawer 内容 */
const list = computed<ImGroupRequestRespVO[]>(() =>
groupRequestStore.getUnhandledListByGroupId(props.groupId)
)
/** 切群时收起 */
watch(
() => props.groupId,
() => {
expanded.value = false
}
)
/** 同意申请 */
async function handleAgree(item: ImGroupRequestRespVO) {
if (actingId.value !== null) return
actingId.value = item.id
try {
await groupRequestStore.agreeRequest(item.id)
message.success('已同意')
if (list.value.length === 0) {
expanded.value = false
}
} finally {
actingId.value = null
}
}
/** 拒绝申请 */
async function handleRefuse(item: ImGroupRequestRespVO) {
if (actingId.value !== null) return
let handleContent = ''
try {
const result = await message.prompt('请输入拒绝理由(可选)', '拒绝申请')
handleContent = result.value || ''
} catch {
return
}
actingId.value = item.id
try {
await groupRequestStore.refuseRequest(item.id, handleContent || undefined)
message.success('已拒绝')
if (list.value.length === 0) {
expanded.value = false
}
} finally {
actingId.value = null
}
}
// todo @AI style unocss
</script>
<style scoped>
/* 容器align-items flex-start 让胶囊靠左、不占整行;高度由内容撑开,与置顶消息横幅节奏对齐 */
.im-conversation-group-request {
position: relative;
flex-shrink: 0;
display: flex;
flex-direction: column;
@ -173,28 +79,31 @@ async function handleRefuse(item: ImGroupRequestRespVO) {
background-color: var(--el-fill-color-light);
}
/* 胶囊本体内容自适应宽度padding / 圆角 / 阴影对齐 ConversationGroupPinned 的 __row */
.im-conversation-group-request__row {
display: flex;
display: inline-flex;
align-items: center;
gap: 6px;
width: 360px;
padding: 6px 12px;
background-color: var(--el-bg-color);
border-radius: 10px;
font-size: 13px;
color: var(--el-text-color-primary);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.im-conversation-group-request__row--clickable {
cursor: pointer;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: background-color 0.15s;
}
.im-conversation-group-request__row--clickable:hover {
.im-conversation-group-request__row:hover {
background-color: var(--el-fill-color-lighter);
}
/* 绿色「加好友」icon与置顶消息黄色 pushpin 同节奏仅换色调svg 强制 currentColor 应对暗色覆盖 */
.im-conversation-group-request__icon {
flex-shrink: 0;
color: var(--el-color-primary);
color: var(--el-color-success);
}
.im-conversation-group-request__icon :deep(svg) {
fill: currentColor !important;
}
.im-conversation-group-request__text {
@ -209,59 +118,4 @@ async function handleRefuse(item: ImGroupRequestRespVO) {
flex-shrink: 0;
color: var(--el-text-color-placeholder);
}
.im-conversation-group-request__list {
position: absolute;
top: 100%;
margin-top: -1px;
left: 6px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 8px;
width: 380px;
max-height: 360px;
overflow-y: auto;
padding: 12px;
border-radius: 12px;
background-color: var(--el-bg-color);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.im-conversation-group-request__item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
background-color: var(--el-fill-color-light);
border-radius: 8px;
}
.im-conversation-group-request__item-body {
flex: 1;
min-width: 0;
}
.im-conversation-group-request__item-name {
font-size: 13px;
color: var(--el-text-color-primary);
}
.im-conversation-group-request__item-inviter {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-left: 4px;
}
.im-conversation-group-request__item-msg {
margin-top: 2px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.im-conversation-group-request__item-actions {
flex-shrink: 0;
display: flex;
gap: 6px;
}
</style>