feat(im): 增加群邀请的功能

im
YunaiV 2026-04-30 15:47:32 +08:00
parent 0ab8b292f2
commit 368b385267
6 changed files with 447 additions and 440 deletions

View File

@ -6,25 +6,17 @@
- 已勾选预览
- 提交 createGroup inviteGroupMember最后让父页 reload
-->
<el-dialog
v-model="visible"
title="新建群聊"
width="620px"
:close-on-click-modal="false"
>
<el-dialog v-model="visible" title="新建群聊" width="620px" :close-on-click-modal="false">
<div class="flex flex-col gap-3">
<el-input
v-model="groupName"
placeholder="请输入群名称"
maxlength="20"
show-word-limit
/>
<el-input v-model="groupName" placeholder="请输入群名称" maxlength="20" show-word-limit />
<div class="flex gap-2.5">
<div class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]">
<div
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
>
<el-input v-model="searchText" placeholder="搜索好友" size="small" clearable>
<template #suffix>
<el-icon><Search /></el-icon>
<Icon icon="ant-design:search-outlined" />
</template>
</el-input>
<el-scrollbar class="h-[400px]">
@ -34,7 +26,7 @@
:friend="f"
:menu="false"
:active="false"
@click="toggleCheck(f)"
@click="handleToggleCheck(f)"
>
<el-checkbox
:model-value="f.isCheck"
@ -46,10 +38,12 @@
</div>
<div class="flex items-center text-lg text-[#409eff]">
<el-icon><DArrowRight /></el-icon>
<Icon icon="ant-design:double-right-outlined" />
</div>
<div class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]">
<div
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
>
<div
class="h-10 pl-2.5 leading-10 text-13px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
>
@ -77,14 +71,16 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { Search, DArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import Icon from '@/components/Icon/src/Icon.vue'
import { useMessage } from '@/hooks/web/useMessage'
import { createGroup } from '@/api/im/group'
import { inviteGroupMember } from '@/api/im/group/member'
import FriendItem, { type FriendLite } from '../../friend/components/FriendItem.vue'
import { useGroupStore } from '../../store/groupStore'
import FriendItem from '../friend/FriendItem.vue'
import type { FriendLite } from '../../types'
defineOptions({ name: 'ImCreateGroupDialog' })
defineOptions({ name: 'ImGroupCreateDialog' })
interface FriendCheckable extends FriendLite {
isCheck?: boolean
@ -93,8 +89,7 @@ interface FriendCheckable extends FriendLite {
const props = withDefaults(
defineProps<{
modelValue: boolean
/** 全量好友(由调用方从 friendStore 传入) */
friends?: FriendLite[]
friends?: FriendLite[] // friendStore
}>(),
{
friends: () => []
@ -103,20 +98,23 @@ const props = withDefaults(
const emit = defineEmits<{
'update:modelValue': [value: boolean]
/** 创建成功,携带新群编号 */
created: [groupId: number]
created: [groupId: number] //
}>()
const message = useMessage()
const groupStore = useGroupStore()
/** 弹窗显隐:把父侧 v-model 转双向计算 */
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
set: (value) => emit('update:modelValue', value)
})
const groupName = ref('')
const searchText = ref('')
const submitting = ref(false)
/** 工作副本(带 isCheck 标记),与 prop 隔离 */
const workingFriends = ref<FriendCheckable[]>([])
// TODO @AIchecked
const workingFriends = ref<FriendCheckable[]>([]) // isCheck prop
watch(
visible,
@ -133,40 +131,52 @@ watch(
{ immediate: true }
)
/** 左侧展示的好友:按搜索关键字过滤 workingFriends */
const shownFriends = computed(() =>
workingFriends.value.filter((f) => f.nickname.includes(searchText.value))
)
/** 已勾选的好友:右侧预览 + 提交时取 memberUserIds */
const checkedFriends = computed(() => workingFriends.value.filter((f) => f.isCheck))
function toggleCheck(f: FriendCheckable) {
/** 行点击:切换勾选态,让点击整行与点 checkbox 等价 */
function handleToggleCheck(f: FriendCheckable) {
f.isCheck = !f.isCheck
}
/** 创建群聊:建群 → 拉人 → upsert groupStore最后 emit('created') 让父页跳转新会话 */
async function handleOk() {
const name = groupName.value.trim()
if (!name) {
ElMessage.warning('请输入群名称')
message.warning('请输入群名称')
return
}
const memberUserIds = checkedFriends.value.map((f) => f.id)
if (memberUserIds.length === 0) {
ElMessage.warning('请至少选择一位好友')
message.warning('请至少选择一位好友')
return
}
submitting.value = true
try {
// 1.1
const group = await createGroup({ name })
if (!group?.id) {
throw new Error('创建群失败:未返回群编号')
}
// 1.2
await inviteGroupMember({ groupId: group.id, memberUserIds })
ElMessage.success('群聊创建成功')
// 2.1 upsert groupStore fetchGroups VO
groupStore.upsertGroup({
id: group.id,
name: group.name,
avatar: group.avatar,
notice: group.notice,
ownerUserId: group.ownerUserId
})
// 2.2 + emit +
message.success('群聊创建成功')
emit('created', group.id)
visible.value = false
} catch (e: any) {
console.error('[IM] 创建群失败', e)
ElMessage.error(e?.message || '创建群失败')
} finally {
submitting.value = false
}

View File

@ -0,0 +1,174 @@
<template>
<!--
邀请好友入群对话框
- 好友列表 checkbox
- 已勾选预览
- 已在群内的好友标记为 disabled
- TODO 接入 /im/group/inviteTODO 这个是不是已经接入了
-->
<el-dialog v-model="visible" title="邀请好友" width="620px" :close-on-click-modal="false">
<div class="flex gap-2.5">
<div
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
>
<el-input v-model="searchText" placeholder="搜索好友" size="small" clearable>
<template #suffix>
<Icon icon="ant-design:search-outlined" />
</template>
</el-input>
<el-scrollbar class="h-[400px]">
<!-- TODO @ai: friend? -->
<FriendItem
v-for="f in shownFriends"
:key="f.id"
:friend="f"
:menu="false"
:active="false"
@click="handleToggleCheck(f)"
>
<!-- TODO @AIchecked -->
<el-checkbox
:model-value="f.isCheck"
:disabled="f.disabled"
@click.stop
@change="(v) => (f.isCheck = !!v)"
/>
</FriendItem>
</el-scrollbar>
</div>
<div class="flex items-center text-lg text-[#409eff]">
<Icon icon="ant-design:double-right-outlined" />
</div>
<div
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
>
<div
class="h-10 pl-2.5 leading-10 text-13px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
>
已勾选 {{ checkCount }} 位好友
</div>
<el-scrollbar class="h-[400px]">
<FriendItem
v-for="f in checkedFriends"
:key="f.id"
:friend="f"
:menu="false"
:active="false"
/>
</el-scrollbar>
</div>
</div>
<template #footer>
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handleOk"> </el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import Icon from '@/components/Icon/src/Icon.vue'
import { useMessage } from '@/hooks/web/useMessage'
import { CommonStatusEnum } from '@/utils/constants'
import FriendItem from '../friend/FriendItem.vue'
import type { FriendLite } from '../../types'
import type { GroupMemberLite } from './GroupMember.vue'
defineOptions({ name: 'ImGroupMemberAddDialog' })
interface FriendCheckable extends FriendLite {
isCheck?: boolean
disabled?: boolean
}
const props = withDefaults(
defineProps<{
modelValue: boolean
groupId?: number
members?: GroupMemberLite[] //
friends?: FriendLite[] // friendStore
}>(),
{
members: () => [],
friends: () => []
}
)
const emit = defineEmits<{
'update:modelValue': [value: boolean]
reload: [friendIds: number[]] // id
}>()
const message = useMessage()
/** 弹窗显隐:把父侧 v-model 转双向计算 */
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const searchText = ref('')
/** 本地工作副本(带 isCheck / disabled 标记);和 props.friends 区分以避免直接改 prop */
const workingFriends = ref<FriendCheckable[]>([])
watch(
visible,
(v) => {
if (!v) {
return
}
workingFriends.value = props.friends
.filter((friend) => !friend.deleted)
.map((friend) => {
const inGroup = props.members.some(
(member) => member.status !== CommonStatusEnum.DISABLE && member.userId === friend.id
)
return {
...friend,
disabled: inGroup,
isCheck: inGroup
}
})
},
{ immediate: true }
)
/** 左侧展示的好友:按搜索关键字过滤 workingFriends */
const shownFriends = computed(() =>
workingFriends.value.filter((f) => f.nickname.includes(searchText.value))
)
/** 本次将被邀请的好友:勾选 + 非已在群成员disabled 不计入) */
const checkedFriends = computed(() => workingFriends.value.filter((f) => f.isCheck && !f.disabled))
/** 已勾选数量:右侧标题展示 */
const checkCount = computed(() => checkedFriends.value.length)
/** 行点击切换勾选态已在群disabled的不响应 */
function handleToggleCheck(f: FriendCheckable) {
if (!f.disabled) {
f.isCheck = !f.isCheck
}
}
// TODO @AI
/**
* 邀请入群占位实现 reload 让父侧关弹窗 / 刷新
*
* TODO 接入 /im/group/invite
*/
async function handleOk() {
const ids = checkedFriends.value.map((f) => f.id)
if (ids.length === 0) {
message.warning('请选择至少一个好友')
return
}
message.info('邀请入群接口待接入,当前为占位实现')
emit('reload', ids)
visible.value = false
}
</script>

View File

@ -0,0 +1,44 @@
<template>
<!--
群成员宫格单元
- 宫格展示的最小单位头像在上名字在下列宽 = size + 16自适应 size 留呼吸空间
- GroupMemberSelector 右侧已选区ConversationGroupSide 群成员区循环使用
-->
<div
class="relative flex flex-col items-center px-0.5 py-1"
:style="{ width: `${size! + 16}px` }"
>
<UserAvatar
:id="member.userId"
:url="member.avatar"
:name="member.nickname"
:size="size"
:clickable="clickable"
/>
<div
class="w-full mt-1 overflow-hidden text-12px leading-[18px] text-center truncate text-[var(--el-text-color-regular)]"
>
{{ member.showName }}
</div>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import UserAvatar from '../user/UserAvatar.vue'
import type { GroupMemberLite } from './GroupMember.vue'
defineOptions({ name: 'ImGroupMemberGrid' })
withDefaults(
defineProps<{
member: GroupMemberLite
clickable?: boolean // UserInfoCard
size?: number // 38 50 PC
}>(),
{
clickable: false,
size: 38
}
)
</script>

View File

@ -0,0 +1,184 @@
<template>
<!--
群成员选择器
- 搜索 + 群成员列表 checkbox
- 已勾选的成员宫格预览
- 确定时 emit complete抛出选中的成员列表
-->
<el-dialog v-model="visible" :title="title" width="700px" :close-on-click-modal="false">
<div class="flex gap-2.5">
<div
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
>
<el-input v-model="searchText" placeholder="搜索" clearable>
<template #suffix>
<Icon icon="ant-design:search-outlined" />
</template>
</el-input>
<div class="h-[400px]">
<PagedScroller :items="showMembers" :page-size="30">
<template #default="{ item }">
<GroupMemberItem
:member="item as GroupMemberFlag"
:height="46"
@click="handleToggleCheck(item as GroupMemberFlag)"
>
<el-checkbox
:model-value="(item as GroupMemberFlag).checked"
:disabled="(item as GroupMemberFlag).locked"
@click.stop
@change="(val: boolean) => handleCheckChange(item as GroupMemberFlag, val)"
/>
</GroupMemberItem>
</template>
</PagedScroller>
</div>
</div>
<div class="flex items-center text-xl text-[#409eff]">
<Icon icon="ant-design:double-right-outlined" />
</div>
<div
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
>
<div
class="h-10 pl-2.5 leading-10 text-13px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
>
已勾选 {{ checkedMembers.length }} 位成员
</div>
<el-scrollbar class="h-[400px]">
<div class="flex flex-wrap p-2.5">
<GroupMemberGrid v-for="m in checkedMembers" :key="m.userId" :member="m" />
</div>
</el-scrollbar>
</div>
</div>
<template #footer>
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handleOk"> </el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import Icon from '@/components/Icon/src/Icon.vue'
import { useMessage } from '@/hooks/web/useMessage'
import { CommonStatusEnum } from '@/utils/constants'
import GroupMemberItem from './GroupMemberItem.vue'
import GroupMemberGrid from './GroupMemberGrid.vue'
import PagedScroller from '../PagedScroller.vue'
import type { GroupMemberLite } from './GroupMember.vue'
defineOptions({ name: 'ImGroupMemberSelector' })
/** 选择器内部扩展:加上 checked / locked / hide 标记 */
export interface GroupMemberFlag extends GroupMemberLite {
checked?: boolean
locked?: boolean
hide?: boolean
}
const props = withDefaults(
defineProps<{
modelValue: boolean
title?: string
members?: GroupMemberLite[] // status/avatar
checkedIds?: number[] // userId
lockedIds?: number[] // userId
hideIds?: number[] // userId
maxSize?: number // -1
}>(),
{
title: '选择成员',
members: () => [],
checkedIds: () => [],
lockedIds: () => [],
hideIds: () => [],
maxSize: -1
}
)
const emit = defineEmits<{
'update:modelValue': [value: boolean]
complete: [members: GroupMemberFlag[]] // ""
}>()
const message = useMessage()
/** 弹窗显隐:把父侧 v-model 转双向计算 */
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const searchText = ref('')
const workingMembers = ref<GroupMemberFlag[]>([])
watch(
visible,
(v) => {
if (v) rebuild()
},
{ immediate: true }
)
/** 重建工作副本:把 checkedIds / lockedIds / hideIds 翻译成每个 member 的 flag */
function rebuild() {
// TODO @AImember m
workingMembers.value = props.members.map((m) => ({
...m,
checked: props.checkedIds.some((id) => id === m.userId),
locked: props.lockedIds.some((id) => id === m.userId),
hide: props.hideIds.some((id) => id === m.userId)
}))
}
/** 当前展示的成员:过滤 hide / DISABLE / 不匹配关键字 */
const showMembers = computed(() =>
workingMembers.value.filter(
(member) =>
!member.hide &&
member.status !== CommonStatusEnum.DISABLE &&
member.showName.includes(searchText.value)
)
)
/** 已勾选成员:右侧宫格预览 + complete 抛参 */
const checkedMembers = computed(() => workingMembers.value.filter((m) => m.checked))
/** 落勾选并校验上限:超过 maxSize 时自动取消并提示,避免出现"勾上但实际不算"的中间态 */
// TODO @AImember
// TODO @AIval checked
function applyCheck(m: GroupMemberFlag, val: boolean) {
m.checked = val
if (props.maxSize > 0 && checkedMembers.value.length > props.maxSize) {
message.error(`最多选择 ${props.maxSize} 位成员`)
m.checked = false
}
}
/** 行点击切换勾选态locked 的不响应 */
// TODO @AImember
function handleToggleCheck(m: GroupMemberFlag) {
if (m.locked) {
return
}
applyCheck(m, !m.checked)
}
/** checkbox change直接落 vallocked 已由 disabled 拦截) */
// TODO @AImember
function handleCheckChange(m: GroupMemberFlag, val: boolean) {
applyCheck(m, val)
}
/** 确定:把已勾选成员通过 complete 抛给父侧 */
function handleOk() {
emit('complete', checkedMembers.value)
visible.value = false
}
</script>

View File

@ -1,192 +0,0 @@
<template>
<!--
邀请好友入群对话框对应 boxim group/AddGroupMember.vue
- 好友列表 checkbox
- 已勾选预览
- 已在群内的好友标记为 disabled
- TODO 接入 /im/group/invite
-->
<el-dialog
v-model="visible"
title="邀请好友"
width="620px"
:close-on-click-modal="false"
>
<div class="im-add-group-member">
<div class="im-add-group-member__left">
<el-input v-model="searchText" placeholder="搜索好友" size="small" clearable>
<template #suffix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-scrollbar class="im-add-group-member__scroll">
<FriendItem
v-for="f in shownFriends"
:key="f.id"
:friend="f"
:menu="false"
:active="false"
@click="toggleCheck(f)"
>
<el-checkbox
:model-value="f.isCheck"
:disabled="f.disabled"
@click.stop
@change="(v: boolean) => (f.isCheck = v)"
/>
</FriendItem>
</el-scrollbar>
</div>
<div class="im-add-group-member__arrow">
<el-icon><DArrowRight /></el-icon>
</div>
<div class="im-add-group-member__right">
<div class="im-add-group-member__tip">已勾选 {{ checkCount }} 位好友</div>
<el-scrollbar class="im-add-group-member__scroll">
<FriendItem
v-for="f in checkedFriends"
:key="f.id"
:friend="f"
:menu="false"
:active="false"
/>
</el-scrollbar>
</div>
</div>
<template #footer>
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handleOk"> </el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { Search, DArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import FriendItem, { type FriendLite } from '../../friend/components/FriendItem.vue'
import type { GroupMemberLite } from '../../chat/components/ChatGroupMember.vue'
defineOptions({ name: 'ImAddGroupMemberDialog' })
interface FriendCheckable extends FriendLite {
isCheck?: boolean
disabled?: boolean
}
const props = withDefaults(
defineProps<{
modelValue: boolean
groupId?: string | number
/** 本群现有成员,用来判断好友是否已在群里 */
members?: GroupMemberLite[]
/** 全量好友(由调用方从 friendStore 传入) */
friends?: FriendLite[]
}>(),
{
members: () => [],
friends: () => []
}
)
const emit = defineEmits<{
'update:modelValue': [value: boolean]
/** 邀请完成,携带被邀请的好友 id 列表 */
reload: [friendIds: (string | number)[]]
}>()
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const searchText = ref('')
const friends = ref<FriendCheckable[]>([])
watch(
visible,
(v) => {
if (!v) return
friends.value = props.friends
.filter((f) => !f.deleted)
.map((f) => {
const inGroup = props.members.some(
(m) => !m.quit && String(m.userId) === String(f.id)
)
return {
...f,
disabled: inGroup,
isCheck: inGroup
}
})
},
{ immediate: true }
)
const shownFriends = computed(() =>
friends.value.filter((f) => f.nickName.includes(searchText.value))
)
const checkedFriends = computed(() =>
friends.value.filter((f) => f.isCheck && !f.disabled)
)
const checkCount = computed(() => checkedFriends.value.length)
function toggleCheck(f: FriendCheckable) {
if (!f.disabled) f.isCheck = !f.isCheck
}
// TODO /im/group/invite
async function handleOk() {
const ids = checkedFriends.value.map((f) => f.id)
if (ids.length === 0) {
ElMessage.warning('请选择至少一个好友')
return
}
ElMessage.info('邀请入群接口待接入,当前为占位实现')
emit('reload', ids)
visible.value = false
}
</script>
<style scoped>
.im-add-group-member {
display: flex;
gap: 10px;
}
.im-add-group-member__left,
.im-add-group-member__right {
display: flex;
flex-direction: column;
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.im-add-group-member__scroll {
height: 400px;
}
.im-add-group-member__arrow {
display: flex;
align-items: center;
font-size: 18px;
color: #409eff;
}
.im-add-group-member__tip {
height: 40px;
padding-left: 10px;
line-height: 40px;
font-size: 13px;
color: #909399;
border-bottom: 1px solid #f0f2f5;
}
</style>

View File

@ -1,213 +0,0 @@
<template>
<!--
群成员选择器对应 boxim group/GroupMemberSelector.vue
- 搜索 + 群成员列表 checkbox
- 已勾选的成员宫格预览
- 确定时 emit complete抛出选中的成员列表
-->
<el-dialog
v-model="visible"
:title="title"
width="700px"
:close-on-click-modal="false"
>
<div class="im-group-member-selector">
<div class="im-group-member-selector__left">
<el-input v-model="searchText" placeholder="搜索" clearable>
<template #suffix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<div class="im-group-member-selector__scroll">
<PagedScroller :items="showMembers" :page-size="30">
<template #default="{ item }">
<GroupMemberItem
:member="(item as GroupMemberFlag)"
:height="46"
@click="toggleCheck(item as GroupMemberFlag)"
>
<el-checkbox
:model-value="(item as GroupMemberFlag).checked"
:disabled="(item as GroupMemberFlag).locked"
@click.stop
@change="(val: boolean) => onCheckChange(item as GroupMemberFlag, val)"
/>
</GroupMemberItem>
</template>
</PagedScroller>
</div>
</div>
<div class="im-group-member-selector__arrow">
<el-icon><DArrowRight /></el-icon>
</div>
<div class="im-group-member-selector__right">
<div class="im-group-member-selector__tip">已勾选 {{ checkedMembers.length }} 位成员</div>
<el-scrollbar class="im-group-member-selector__scroll">
<div class="im-group-member-selector__grid">
<GroupMemberGrid
v-for="m in checkedMembers"
:key="m.userId"
:member="m"
/>
</div>
</el-scrollbar>
</div>
</div>
<template #footer>
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handleOk"> </el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { Search, DArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import GroupMemberItem from './GroupMemberItem.vue'
import GroupMemberGrid from './GroupMemberGrid.vue'
import PagedScroller from '../../../components/PagedScroller.vue'
import type { GroupMemberLite } from '../../chat/components/ChatGroupMember.vue'
defineOptions({ name: 'ImGroupMemberSelector' })
/** 选择器内部扩展:加上 checked / locked / hide 标记 */
export interface GroupMemberFlag extends GroupMemberLite {
checked?: boolean
locked?: boolean
hide?: boolean
}
const props = withDefaults(
defineProps<{
modelValue: boolean
title?: string
/** 传入的群成员列表(已经有 quit/headImage 等基础字段) */
members?: GroupMemberLite[]
/** 默认选中的 userId 列表 */
checkedIds?: (string | number)[]
/** 锁定的 userId 列表(不能取消) */
lockedIds?: (string | number)[]
/** 隐藏的 userId 列表(不展示) */
hideIds?: (string | number)[]
/** 最多可选数量,-1 表示不限制 */
maxSize?: number
}>(),
{
title: '选择成员',
members: () => [],
checkedIds: () => [],
lockedIds: () => [],
hideIds: () => [],
maxSize: -1
}
)
const emit = defineEmits<{
'update:modelValue': [value: boolean]
/** 点击"确定"时抛出被勾选的成员列表 */
complete: [members: GroupMemberFlag[]]
}>()
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const searchText = ref('')
const workingMembers = ref<GroupMemberFlag[]>([])
watch(
visible,
(v) => {
if (v) rebuild()
},
{ immediate: true }
)
function rebuild() {
workingMembers.value = props.members.map((m) => ({
...m,
checked: props.checkedIds.some((id) => String(id) === String(m.userId)),
locked: props.lockedIds.some((id) => String(id) === String(m.userId)),
hide: props.hideIds.some((id) => String(id) === String(m.userId))
}))
}
const showMembers = computed(() =>
workingMembers.value.filter(
(m) => !m.hide && !m.quit && m.showNickName.includes(searchText.value)
)
)
const checkedMembers = computed(() => workingMembers.value.filter((m) => m.checked))
function toggleCheck(m: GroupMemberFlag) {
if (m.locked) return
m.checked = !m.checked
if (props.maxSize > 0 && checkedMembers.value.length > props.maxSize) {
ElMessage.error(`最多选择 ${props.maxSize} 位成员`)
m.checked = false
}
}
function onCheckChange(m: GroupMemberFlag, val: boolean) {
m.checked = val
if (props.maxSize > 0 && checkedMembers.value.length > props.maxSize) {
ElMessage.error(`最多选择 ${props.maxSize} 位成员`)
m.checked = false
}
}
function handleOk() {
emit('complete', checkedMembers.value)
visible.value = false
}
</script>
<style scoped>
.im-group-member-selector {
display: flex;
gap: 10px;
}
.im-group-member-selector__left,
.im-group-member-selector__right {
display: flex;
flex-direction: column;
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.im-group-member-selector__scroll {
height: 400px;
}
.im-group-member-selector__arrow {
display: flex;
align-items: center;
font-size: 20px;
color: #409eff;
}
.im-group-member-selector__tip {
height: 40px;
padding-left: 10px;
line-height: 40px;
font-size: 13px;
color: #909399;
border-bottom: 1px solid #f0f2f5;
}
.im-group-member-selector__grid {
display: flex;
flex-wrap: wrap;
padding: 10px;
}
</style>