admin-vue3/src/views/im/home/components/group/GroupCreateDialog.vue

246 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<!--
新建群聊对话框
- 顶部群名称输入
- 好友列表checkbox 前置
- 已勾选预览每行可 x 移除locked 不渲染 x
- 提交 createGroup inviteGroupMember最后让父页 reload
- lockedIds锁定不可取消的好友 id私聊侧 "+创建群" 入口用来锁定对方
-->
<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 />
<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 #prefix>
<Icon
icon="ant-design:search-outlined"
class="text-[var(--el-text-color-placeholder)]"
/>
</template>
</el-input>
<el-scrollbar class="h-[400px]">
<FriendItem
v-for="friend in shownFriends"
:key="friend.id"
:friend="friend"
:menu="false"
:active="false"
@click="handleToggleCheck(friend)"
>
<template #prefix>
<el-checkbox
:model-value="friend.checked"
:disabled="friend.disabled"
@click.stop
@change="(value) => handleCheckChange(friend, !!value)"
/>
</template>
</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)]"
>
<!-- 标题高度对齐左侧 el-input default32px保证两侧第一项起点在同一水平 -->
<div
class="h-8 pl-2.5 leading-8 text-13px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
>
已勾选 {{ checkedFriends.length }} 位好友
</div>
<el-scrollbar class="h-[400px]">
<FriendItem
v-for="friend in checkedFriends"
:key="friend.id"
:friend="friend"
:menu="false"
:active="false"
>
<!-- locked 的好友不渲染 x避免误以为可移除 -->
<Icon
v-if="!friend.disabled"
icon="ant-design:close-outlined"
class="im-group-create-dialog__remove"
@click.stop="handleUncheck(friend)"
/>
</FriendItem>
</el-scrollbar>
</div>
</div>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" :disabled="!canSubmit" @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 { createGroup } from '@/api/im/group'
import { inviteGroupMember } from '@/api/im/group/member'
import { useGroupStore } from '../../store/groupStore'
import FriendItem from '../friend/FriendItem.vue'
import type { FriendLite } from '../../types'
defineOptions({ name: 'ImGroupCreateDialog' })
interface FriendCheckable extends FriendLite {
checked?: boolean
disabled?: boolean // locked 的好友:勾选态由 lockedIds 强制为 trueUI 上 checkbox / x 都不响应
}
const props = withDefaults(
defineProps<{
modelValue: boolean
friends?: FriendLite[] // 全量好友(由调用方从 friendStore 传入)
lockedIds?: number[] // 锁定的好友 id自动勾选 + 不可取消(私聊侧 "+创建群" 用来锁定对方)
}>(),
{
friends: () => [],
lockedIds: () => []
}
)
const emit = defineEmits<{
'update:modelValue': [value: boolean]
created: [groupId: number] // 创建成功,携带新群编号
}>()
const message = useMessage()
const groupStore = useGroupStore()
/** 弹窗显隐:把父侧 v-model 转双向计算 */
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const groupName = ref('')
const searchText = ref('')
const submitting = ref(false)
const workingFriends = ref<FriendCheckable[]>([]) // 工作副本(带 checked / disabled 标记),与 prop 隔离
watch(
visible,
(open) => {
if (!open) {
return
}
groupName.value = ''
searchText.value = ''
workingFriends.value = props.friends
.filter((friend) => !friend.deleted)
.map((friend) => {
const locked = props.lockedIds.some((id) => id === friend.id)
return { ...friend, checked: locked, disabled: locked }
})
},
{ immediate: true }
)
/** 左侧展示的好友:按搜索关键字过滤 workingFriends */
const shownFriends = computed(() =>
workingFriends.value.filter((friend) => friend.nickname.includes(searchText.value))
)
/** 已勾选的好友:右侧预览 + 提交时取 memberUserIds */
const checkedFriends = computed(() => workingFriends.value.filter((friend) => friend.checked))
/**
* 完成按钮可点:群名非空 + 至少有 1 个非 locked 勾选
*
* locked 是入口侧自动选的(如私聊对方),不算"用户主动选择"——否则用户什么都没勾就能建 2 人群,体验上等于私聊
*/
const canSubmit = computed(() => {
if (!groupName.value.trim()) {
return false
}
return checkedFriends.value.some((friend) => !friend.disabled)
})
/** 行点击切换勾选态locked 的不响应 */
function handleToggleCheck(friend: FriendCheckable) {
if (friend.disabled) {
return
}
friend.checked = !friend.checked
}
/** checkbox change直接落 valuelocked 已由 :disabled 拦截,这里再守一层) */
function handleCheckChange(friend: FriendCheckable, value: boolean) {
if (friend.disabled) {
return
}
friend.checked = value
}
/** 右侧 x 点击取消勾选locked 不渲染 x到这里说明非 locked */
function handleUncheck(friend: FriendCheckable) {
friend.checked = false
}
/** 创建群聊:建群 → 拉人 → upsert groupStore最后 emit('created') 让父页跳转新会话 */
async function handleOk() {
const name = groupName.value.trim()
const memberUserIds = checkedFriends.value.map((friend) => friend.id)
// canSubmit 已挡住空状态,这里再守一道防止 disabled 被外部绕过
if (!name || memberUserIds.length === 0) {
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 })
// 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
} finally {
submitting.value = false
}
}
</script>
<style scoped>
/* 右侧已选行的 x默认浅灰hover 转危险色,提示"点了就移除" */
.im-group-create-dialog__remove {
font-size: 14px;
color: var(--el-text-color-placeholder);
cursor: pointer;
transition: color 0.15s;
}
.im-group-create-dialog__remove:hover {
color: var(--el-color-danger);
}
</style>