feat(im): fuzzy member search with chip tags in create-group dialog
- ImManagementView: replace textarea with debounced search input + closable el-tag chips - im.ts: add searchUsers(keyword) API call to im-service admin search endpoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
e27a9ef917
当前提交
6c44354f2b
@ -73,4 +73,11 @@ export const imAdminApi = {
|
||||
{ params: { appId } },
|
||||
)
|
||||
},
|
||||
|
||||
searchUsers(appId: string, keyword: string, size = 20) {
|
||||
return imClient.get<{ data: ImUser[] }>(
|
||||
'/api/im/admin/users/search',
|
||||
{ params: { appId, keyword, size } },
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@ -97,13 +97,47 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- Create Group Dialog -->
|
||||
<el-dialog v-model="showCreateGroup" title="创建群组" width="420px">
|
||||
<el-dialog v-model="showCreateGroup" title="创建群组" width="480px" @closed="resetCreateGroupForm">
|
||||
<el-form :model="createGroupForm" label-width="90px">
|
||||
<el-form-item label="群名称"><el-input v-model="createGroupForm.name" /></el-form-item>
|
||||
<el-form-item label="创建者ID"><el-input v-model="createGroupForm.creatorId" /></el-form-item>
|
||||
<el-form-item label="初始成员">
|
||||
<el-input v-model="createGroupForm.memberIdsRaw" type="textarea" :rows="3"
|
||||
placeholder="每行一个用户ID" />
|
||||
<el-form-item label="搜索成员">
|
||||
<el-input
|
||||
v-model="memberSearchKeyword"
|
||||
:prefix-icon="Search"
|
||||
placeholder="输入昵称或用户ID搜索"
|
||||
clearable
|
||||
@input="onMemberSearchInput"
|
||||
/>
|
||||
<!-- Search results list -->
|
||||
<div v-if="memberSearchResults.length > 0" class="search-results">
|
||||
<div
|
||||
v-for="user in memberSearchResults"
|
||||
:key="user.userId"
|
||||
class="search-result-item"
|
||||
@click="addMember(user)"
|
||||
>
|
||||
<span class="result-nickname">{{ user.nickname || user.userId }}</span>
|
||||
<span class="result-userid">{{ user.userId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else-if="memberSearchKeyword && !searchingMembers && memberSearchResults.length === 0"
|
||||
description="未找到匹配用户" :image-size="48" style="padding:8px 0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="已选成员">
|
||||
<div class="selected-members">
|
||||
<el-tag
|
||||
v-for="m in selectedMembers"
|
||||
:key="m.userId"
|
||||
closable
|
||||
type="primary"
|
||||
style="margin:3px"
|
||||
@close="removeMember(m.userId)"
|
||||
>
|
||||
{{ m.nickname || m.userId }}
|
||||
</el-tag>
|
||||
<span v-if="selectedMembers.length === 0" class="no-members-hint">暂未选择成员</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@ -118,6 +152,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { imAdminApi, type ImUser, type ImGroup, type ImStats } from '@/api/im'
|
||||
|
||||
const route = useRoute()
|
||||
@ -141,7 +176,55 @@ const registerForm = ref({ userId: '', nickname: '' })
|
||||
|
||||
const showCreateGroup = ref(false)
|
||||
const submittingCreateGroup = ref(false)
|
||||
const createGroupForm = ref({ name: '', creatorId: '', memberIdsRaw: '' })
|
||||
const createGroupForm = ref({ name: '', creatorId: '' })
|
||||
|
||||
// Member fuzzy search
|
||||
const memberSearchKeyword = ref('')
|
||||
const memberSearchResults = ref<ImUser[]>([])
|
||||
const searchingMembers = ref(false)
|
||||
const selectedMembers = ref<ImUser[]>([])
|
||||
let memberSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function onMemberSearchInput() {
|
||||
if (memberSearchTimer) clearTimeout(memberSearchTimer)
|
||||
const kw = memberSearchKeyword.value.trim()
|
||||
if (!kw) {
|
||||
memberSearchResults.value = []
|
||||
return
|
||||
}
|
||||
memberSearchTimer = setTimeout(async () => {
|
||||
searchingMembers.value = true
|
||||
try {
|
||||
const res = await imAdminApi.searchUsers(appId, kw)
|
||||
// Exclude already-selected members from results
|
||||
const selectedIds = new Set(selectedMembers.value.map(m => m.userId))
|
||||
memberSearchResults.value = res.data.data.filter(u => !selectedIds.has(u.userId))
|
||||
} catch {
|
||||
memberSearchResults.value = []
|
||||
} finally {
|
||||
searchingMembers.value = false
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function addMember(user: ImUser) {
|
||||
if (!selectedMembers.value.find(m => m.userId === user.userId)) {
|
||||
selectedMembers.value.push(user)
|
||||
}
|
||||
memberSearchResults.value = memberSearchResults.value.filter(u => u.userId !== user.userId)
|
||||
memberSearchKeyword.value = ''
|
||||
}
|
||||
|
||||
function removeMember(userId: string) {
|
||||
selectedMembers.value = selectedMembers.value.filter(m => m.userId !== userId)
|
||||
}
|
||||
|
||||
function resetCreateGroupForm() {
|
||||
createGroupForm.value = { name: '', creatorId: '' }
|
||||
memberSearchKeyword.value = ''
|
||||
memberSearchResults.value = []
|
||||
selectedMembers.value = []
|
||||
}
|
||||
|
||||
const statCards = computed(() => [
|
||||
{ label: '注册用户', value: stats.value?.totalUsers ?? '-' },
|
||||
@ -226,12 +309,10 @@ async function submitCreateGroup() {
|
||||
}
|
||||
submittingCreateGroup.value = true
|
||||
try {
|
||||
const memberIds = createGroupForm.value.memberIdsRaw
|
||||
.split('\n').map(s => s.trim()).filter(Boolean)
|
||||
const memberIds = selectedMembers.value.map(m => m.userId)
|
||||
await imAdminApi.createGroup(appId, createGroupForm.value.name, createGroupForm.value.creatorId, memberIds)
|
||||
ElMessage.success('群组创建成功')
|
||||
showCreateGroup.value = false
|
||||
createGroupForm.value = { name: '', creatorId: '', memberIdsRaw: '' }
|
||||
loadGroups()
|
||||
loadStats()
|
||||
} catch {
|
||||
@ -252,4 +333,34 @@ onMounted(() => {
|
||||
.stat-value { font-size: 28px; font-weight: 800; color: #1f2933; }
|
||||
.stat-label { font-size: 13px; color: #6b7280; }
|
||||
.toolbar { margin-bottom: 12px; }
|
||||
|
||||
.search-results {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.search-result-item:hover { background: #f0f4ff; }
|
||||
.result-nickname { font-weight: 500; color: #303133; }
|
||||
.result-userid { font-size: 12px; color: #909399; }
|
||||
|
||||
.selected-members {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
width: 100%;
|
||||
}
|
||||
.no-members-hint { font-size: 13px; color: #c0c4cc; }
|
||||
</style>
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户