feat(im): align tenant admin im management

这个提交包含在:
XuqmGroup 2026-04-29 10:04:13 +08:00
父节点 c84a27f4ec
当前提交 de89297457
共有 2 个文件被更改,包括 364 次插入152 次删除

查看文件

@ -177,6 +177,18 @@ export const imAdminApi = {
}) })
}, },
updateUser(
appId: string,
userId: string,
form: { nickname?: string; avatar?: string; gender?: string; status?: string },
) {
return imClient.put<{ data: ImUser }>(
`/api/im/admin/users/${encodeURIComponent(userId)}`,
form,
{ params: { appId } },
)
},
updateWebhook(appId: string, webhookId: string, form: WebhookConfigForm) { updateWebhook(appId: string, webhookId: string, form: WebhookConfigForm) {
return imClient.put<{ data: WebhookConfig }>(`/api/im/admin/webhooks/${encodeURIComponent(webhookId)}`, form, { return imClient.put<{ data: WebhookConfig }>(`/api/im/admin/webhooks/${encodeURIComponent(webhookId)}`, form, {
params: { appId }, params: { appId },
@ -301,18 +313,56 @@ export const imAdminApi = {
return imClient.delete<{ data: null }>(`/api/im/admin/groups/${encodeURIComponent(groupId)}`, { params: { appId } }) return imClient.delete<{ data: null }>(`/api/im/admin/groups/${encodeURIComponent(groupId)}`, { params: { appId } })
}, },
registerUser(appId: string, userId: string, nickname?: string, avatar?: string) { registerUser(
appId: string,
userId: string,
nickname?: string,
avatar?: string,
gender?: string,
status?: string,
) {
return imClient.post<{ data: ImUser }>( return imClient.post<{ data: ImUser }>(
'/api/im/admin/users', '/api/im/admin/users',
{ userId, nickname, avatar }, {
userId,
nickname,
avatar,
...(gender ? { gender } : {}),
...(status ? { status } : {}),
},
{ params: { appId } }, { params: { appId } },
) )
}, },
createGroup(appId: string, name: string, creatorId: string, memberIds: string[]) { createGroup(
appId: string,
name: string,
creatorId: string,
memberIds: string[],
groupType?: string,
announcement?: string,
) {
return imClient.post<{ data: ImGroup }>( return imClient.post<{ data: ImGroup }>(
'/api/im/admin/groups', '/api/im/admin/groups',
{ name, creatorId, memberIds }, {
name,
creatorId,
memberIds,
...(groupType ? { groupType } : {}),
...(announcement ? { announcement } : {}),
},
{ params: { appId } },
)
},
updateGroup(
appId: string,
groupId: string,
form: { name?: string; groupType?: string; announcement?: string },
) {
return imClient.put<{ data: ImGroup }>(
`/api/im/admin/groups/${encodeURIComponent(groupId)}`,
form,
{ params: { appId } }, { params: { appId } },
) )
}, },
@ -324,6 +374,32 @@ export const imAdminApi = {
) )
}, },
searchMessages(
appId: string,
filters: {
keyword?: string
chatType?: string
msgType?: string
startTime?: string
endTime?: string
page?: number
size?: number
} = {},
) {
return imClient.get<{ data: PagedResult<ImMessage> }>('/api/im/admin/messages/search', {
params: {
appId,
...(filters.keyword ? { keyword: filters.keyword } : {}),
...(filters.chatType ? { chatType: filters.chatType } : {}),
...(filters.msgType ? { msgType: filters.msgType } : {}),
...(filters.startTime ? { startTime: filters.startTime } : {}),
...(filters.endTime ? { endTime: filters.endTime } : {}),
page: filters.page ?? 0,
size: filters.size ?? 20,
},
})
},
getProfile(appId: string, userId: string) { getProfile(appId: string, userId: string) {
return imClient.get<{ data: ImProfile }>( return imClient.get<{ data: ImProfile }>(
`/api/im/accounts/${encodeURIComponent(userId)}`, `/api/im/accounts/${encodeURIComponent(userId)}`,

查看文件

@ -17,12 +17,19 @@
<el-tabs v-model="activeTab" @tab-change="handleTabChange"> <el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="注册用户" name="users"> <el-tab-pane label="注册用户" name="users">
<div class="toolbar"> <div class="toolbar">
<el-button type="primary" @click="showRegisterUser = true">注册用户</el-button> <el-button type="primary" @click="openCreateUserDialog">新增用户</el-button>
<el-button @click="loadUsers" :loading="loadingUsers">刷新</el-button> <el-button @click="loadUsers" :loading="loadingUsers">刷新</el-button>
</div> </div>
<el-table :data="users" v-loading="loadingUsers" border stripe> <el-table :data="users" v-loading="loadingUsers" border stripe>
<el-table-column prop="userId" label="用户ID" width="180" /> <el-table-column prop="userId" label="用户ID" width="180" />
<el-table-column label="头像" width="90">
<template #default="{ row }">
<el-avatar :size="32" :src="row.avatar || undefined">
{{ userAvatarFallback(row) }}
</el-avatar>
</template>
</el-table-column>
<el-table-column prop="nickname" label="昵称" width="140" /> <el-table-column prop="nickname" label="昵称" width="140" />
<el-table-column prop="gender" label="性别" width="90"> <el-table-column prop="gender" label="性别" width="90">
<template #default="{ row }"> <template #default="{ row }">
@ -41,8 +48,11 @@
{{ formatTime(row.createdAt) }} {{ formatTime(row.createdAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="140" fixed="right"> <el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" size="small" @click="openEditUserDialog(row)">
编辑
</el-button>
<el-button <el-button
link link
:type="row.status === 'ACTIVE' ? 'danger' : 'success'" :type="row.status === 'ACTIVE' ? 'danger' : 'success'"
@ -68,6 +78,7 @@
<el-tab-pane label="群组列表" name="groups"> <el-tab-pane label="群组列表" name="groups">
<div class="toolbar"> <div class="toolbar">
<el-button type="primary" @click="openCreateGroupDialog">创建群组</el-button>
<el-button @click="loadGroups" :loading="loadingGroups">刷新</el-button> <el-button @click="loadGroups" :loading="loadingGroups">刷新</el-button>
</div> </div>
@ -90,8 +101,11 @@
<el-table-column prop="createdAt" label="创建时间" width="180"> <el-table-column prop="createdAt" label="创建时间" width="180">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template> <template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="120" fixed="right"> <el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" size="small" @click="openEditGroupDialog(row)">
编辑
</el-button>
<el-button link type="danger" size="small" @click="dismissGroup(row)"> <el-button link type="danger" size="small" @click="dismissGroup(row)">
解散 解散
</el-button> </el-button>
@ -103,13 +117,13 @@
<el-tab-pane label="消息历史" name="messages"> <el-tab-pane label="消息历史" name="messages">
<el-form :inline="true" :model="historyForm" class="toolbar" label-width="0"> <el-form :inline="true" :model="historyForm" class="toolbar" label-width="0">
<el-form-item> <el-form-item>
<el-input v-model="historyForm.userA" placeholder="用户A" clearable style="width:160px" /> <el-input v-model="historyForm.keyword" placeholder="关键词" clearable style="width:200px" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-input v-model="historyForm.userB" placeholder="用户B" clearable style="width:160px" /> <el-select v-model="historyForm.chatType" placeholder="会话类型" clearable style="width:120px">
</el-form-item> <el-option label="单聊" value="SINGLE" />
<el-form-item> <el-option label="群聊" value="GROUP" />
<el-input v-model="historyForm.keyword" placeholder="关键词" clearable style="width:180px" /> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-select v-model="historyForm.msgType" placeholder="消息类型" clearable style="width:140px"> <el-select v-model="historyForm.msgType" placeholder="消息类型" clearable style="width:140px">
@ -129,7 +143,7 @@
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="loadingMessages" @click="searchMessages"> <el-button type="primary" :loading="loadingMessages" @click="searchMessages">
查询 搜索
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -141,9 +155,9 @@
<el-table-column prop="createdAt" label="时间" width="180"> <el-table-column prop="createdAt" label="时间" width="180">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template> <template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="chatType" label="会话" width="90" />
<el-table-column prop="fromUserId" label="发送者" width="160" /> <el-table-column prop="fromUserId" label="发送者" width="160" />
<el-table-column prop="toId" label="会话ID" width="180" /> <el-table-column prop="toId" label="会话ID" width="180" />
<el-table-column prop="chatType" label="类型" width="90" />
<el-table-column prop="msgType" label="消息类型" width="110" /> <el-table-column prop="msgType" label="消息类型" width="110" />
<el-table-column prop="status" label="状态" width="100" /> <el-table-column prop="status" label="状态" width="100" />
<el-table-column prop="content" label="内容" min-width="260"> <el-table-column prop="content" label="内容" min-width="260">
@ -171,89 +185,6 @@
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="好友申请" name="friend-requests">
<div class="toolbar toolbar-space-between">
<div class="toolbar-group">
<el-select v-model="friendRequestDirection" style="width:140px" @change="loadFriendRequests">
<el-option label="收到的申请" value="incoming" />
<el-option label="发出的申请" value="outgoing" />
</el-select>
<el-button @click="loadFriendRequests" :loading="loadingFriendRequests">刷新</el-button>
</div>
<el-button type="primary" @click="showSendFriendRequest = true">发送好友申请</el-button>
</div>
<el-table :data="friendRequests" v-loading="loadingFriendRequests" border stripe>
<el-table-column prop="createdAt" label="时间" width="180">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column prop="fromUserId" label="申请人" width="160" />
<el-table-column prop="toUserId" label="接收人" width="160" />
<el-table-column prop="remark" label="申请信息" min-width="220" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="110">
<template #default="{ row }">
<el-tag :type="friendRequestTagType(row.status)" size="small">{{ friendRequestStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="reviewedAt" label="处理时间" width="180">
<template #default="{ row }">{{ formatTime(row.reviewedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<template v-if="friendRequestDirection === 'incoming' && row.status === 'PENDING'">
<el-button link type="success" size="small" @click="approveFriend(row)">同意</el-button>
<el-button link type="danger" size="small" @click="declineFriend(row)">拒绝</el-button>
</template>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="入群申请" name="group-requests">
<div class="toolbar toolbar-space-between">
<div class="toolbar-group">
<el-input v-model="groupRequestGroupId" placeholder="群组ID" clearable style="width:260px" />
<el-button @click="loadGroupJoinRequests" :loading="loadingGroupRequests">刷新</el-button>
</div>
<el-button type="primary" @click="showSendGroupJoinRequest = true">发起入群申请</el-button>
</div>
<el-alert
v-if="!groupRequestGroupId.trim()"
type="info"
show-icon
title="请输入群组ID后刷新,才能查看该群的入群申请。"
style="margin-bottom:12px"
/>
<el-table :data="groupJoinRequests" v-loading="loadingGroupRequests" border stripe>
<el-table-column prop="createdAt" label="时间" width="180">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column prop="requesterId" label="申请人" width="160" />
<el-table-column prop="groupId" label="群组ID" width="180" />
<el-table-column prop="remark" label="申请信息" min-width="220" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="110">
<template #default="{ row }">
<el-tag :type="groupRequestTagType(row.status)" size="small">{{ groupRequestStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="reviewedAt" label="处理时间" width="180">
<template #default="{ row }">{{ formatTime(row.reviewedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<template v-if="row.status === 'PENDING'">
<el-button link type="success" size="small" @click="approveGroupJoin(row)">同意</el-button>
<el-button link type="danger" size="small" @click="declineGroupJoin(row)">拒绝</el-button>
</template>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="操作日志" name="logs"> <el-tab-pane label="操作日志" name="logs">
<div class="toolbar"> <div class="toolbar">
<el-button @click="loadOperationLogs" :loading="loadingLogs">刷新</el-button> <el-button @click="loadOperationLogs" :loading="loadingLogs">刷新</el-button>
@ -264,10 +195,20 @@
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template> <template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="operatorId" label="操作者" width="160" /> <el-table-column prop="operatorId" label="操作者" width="160" />
<el-table-column prop="action" label="动作" width="180" /> <el-table-column prop="action" label="动作" width="180">
<el-table-column prop="resourceType" label="资源类型" width="120" /> <template #default="{ row }">
<el-tag size="small" effect="plain">{{ logActionLabel(row.action) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="resourceType" label="资源类型" width="120">
<template #default="{ row }">{{ logResourceLabel(row.resourceType) }}</template>
</el-table-column>
<el-table-column prop="resourceId" label="资源ID" width="220" show-overflow-tooltip /> <el-table-column prop="resourceId" label="资源ID" width="220" show-overflow-tooltip />
<el-table-column prop="detail" label="详情" min-width="220" show-overflow-tooltip /> <el-table-column prop="detail" label="详情" min-width="320">
<template #default="{ row }">
<span class="log-detail">{{ formatLogDetail(row.detail) }}</span>
</template>
</el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
@ -315,22 +256,40 @@
</el-tabs> </el-tabs>
</el-card> </el-card>
<el-dialog v-model="showRegisterUser" title="注册用户" width="420px"> <el-dialog v-model="showRegisterUser" :title="editingUserId ? '编辑用户' : '注册用户'" width="520px" @closed="resetUserForm">
<el-form :model="registerForm" label-width="72px"> <el-form :model="registerForm" label-width="84px">
<el-form-item label="用户ID"> <el-form-item label="用户ID">
<el-input v-model="registerForm.userId" placeholder="全局唯一标识" /> <el-input v-model="registerForm.userId" placeholder="全局唯一标识" :disabled="!!editingUserId" />
</el-form-item> </el-form-item>
<el-form-item label="昵称"> <el-form-item label="昵称">
<el-input v-model="registerForm.nickname" /> <el-input v-model="registerForm.nickname" />
</el-form-item> </el-form-item>
<el-form-item label="头像">
<el-input v-model="registerForm.avatar" placeholder="头像 URL" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="registerForm.gender" style="width:100%">
<el-option label="未知" value="UNKNOWN" />
<el-option label="男" value="MALE" />
<el-option label="女" value="FEMALE" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="registerForm.status" style="width:100%">
<el-option label="正常" value="ACTIVE" />
<el-option label="封禁" value="BANNED" />
</el-select>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="showRegisterUser = false">取消</el-button> <el-button @click="showRegisterUser = false">取消</el-button>
<el-button type="primary" :loading="submittingRegister" @click="submitRegisterUser">注册</el-button> <el-button type="primary" :loading="submittingRegister" @click="submitRegisterUser">
{{ editingUserId ? '保存' : '注册' }}
</el-button>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="showCreateGroup" title="创建群组" width="520px" @closed="resetCreateGroupForm"> <el-dialog v-model="showCreateGroup" title="创建群组" width="600px" @closed="resetCreateGroupForm">
<el-form :model="createGroupForm" label-width="90px"> <el-form :model="createGroupForm" label-width="90px">
<el-form-item label="群名称"> <el-form-item label="群名称">
<el-input v-model="createGroupForm.name" /> <el-input v-model="createGroupForm.name" />
@ -338,6 +297,15 @@
<el-form-item label="创建者ID"> <el-form-item label="创建者ID">
<el-input v-model="createGroupForm.creatorId" /> <el-input v-model="createGroupForm.creatorId" />
</el-form-item> </el-form-item>
<el-form-item label="群类型">
<el-select v-model="createGroupForm.groupType" style="width:100%">
<el-option label="工作群" value="WORK" />
<el-option label="公开群" value="PUBLIC" />
</el-select>
</el-form-item>
<el-form-item label="公告">
<el-input v-model="createGroupForm.announcement" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="搜索成员"> <el-form-item label="搜索成员">
<el-input <el-input
v-model="memberSearchKeyword" v-model="memberSearchKeyword"
@ -386,33 +354,27 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="showSendFriendRequest" title="发送好友申请" width="420px" @closed="resetSendFriendRequestForm"> <el-dialog v-model="showEditGroup" title="编辑群组" width="520px" @closed="resetEditGroupForm">
<el-form :model="sendFriendRequestForm" label-width="88px"> <el-form :model="editGroupForm" label-width="90px">
<el-form-item label="接收人ID">
<el-input v-model="sendFriendRequestForm.toUserId" placeholder="对方用户ID" />
</el-form-item>
<el-form-item label="申请信息">
<el-input v-model="sendFriendRequestForm.remark" type="textarea" :rows="3" placeholder="例如:我是 XXX,想和你加好友" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showSendFriendRequest = false">取消</el-button>
<el-button type="primary" :loading="submittingSendFriendRequest" @click="submitSendFriendRequest">发送</el-button>
</template>
</el-dialog>
<el-dialog v-model="showSendGroupJoinRequest" title="发起入群申请" width="420px" @closed="resetSendGroupJoinRequestForm">
<el-form :model="sendGroupJoinRequestForm" label-width="88px">
<el-form-item label="群组ID"> <el-form-item label="群组ID">
<el-input v-model="sendGroupJoinRequestForm.groupId" placeholder="目标群组ID" /> <el-input :model-value="editingGroup?.id ?? ''" disabled />
</el-form-item> </el-form-item>
<el-form-item label="申请信息"> <el-form-item label="群名称">
<el-input v-model="sendGroupJoinRequestForm.remark" type="textarea" :rows="3" placeholder="例如:希望加入此群进行沟通" /> <el-input v-model="editGroupForm.name" />
</el-form-item>
<el-form-item label="群类型">
<el-select v-model="editGroupForm.groupType" style="width:100%">
<el-option label="工作群" value="WORK" />
<el-option label="公开群" value="PUBLIC" />
</el-select>
</el-form-item>
<el-form-item label="公告">
<el-input v-model="editGroupForm.announcement" type="textarea" :rows="4" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="showSendGroupJoinRequest = false">取消</el-button> <el-button @click="showEditGroup = false">取消</el-button>
<el-button type="primary" :loading="submittingSendGroupJoinRequest" @click="submitSendGroupJoinRequest">发送</el-button> <el-button type="primary" :loading="submittingEditGroup" @click="submitEditGroup">保存</el-button>
</template> </template>
</el-dialog> </el-dialog>
@ -518,20 +480,40 @@ const submittingSendGroupJoinRequest = ref(false)
const sendGroupJoinRequestForm = ref({ groupId: '', remark: '' }) const sendGroupJoinRequestForm = ref({ groupId: '', remark: '' })
const historyForm = ref({ const historyForm = ref({
userA: '',
userB: '',
keyword: '', keyword: '',
chatType: '',
msgType: '', msgType: '',
timeRange: [] as string[] | null, timeRange: [] as string[] | null,
}) })
const showRegisterUser = ref(false) const showRegisterUser = ref(false)
const submittingRegister = ref(false) const submittingRegister = ref(false)
const registerForm = ref({ userId: '', nickname: '' }) const editingUserId = ref<string | null>(null)
const registerForm = ref({
userId: '',
nickname: '',
avatar: '',
gender: 'UNKNOWN' as 'UNKNOWN' | 'MALE' | 'FEMALE',
status: 'ACTIVE' as 'ACTIVE' | 'BANNED',
})
const showCreateGroup = ref(false) const showCreateGroup = ref(false)
const submittingCreateGroup = ref(false) const submittingCreateGroup = ref(false)
const createGroupForm = ref({ name: '', creatorId: '' }) const createGroupForm = ref({
name: '',
creatorId: '',
groupType: 'WORK' as 'WORK' | 'PUBLIC',
announcement: '',
})
const showEditGroup = ref(false)
const submittingEditGroup = ref(false)
const editingGroup = ref<ImGroup | null>(null)
const editGroupForm = ref({
name: '',
groupType: 'WORK' as 'WORK' | 'PUBLIC',
announcement: '',
})
const memberSearchKeyword = ref('') const memberSearchKeyword = ref('')
const memberSearchResults = ref<ImUser[]>([]) const memberSearchResults = ref<ImUser[]>([])
@ -622,12 +604,71 @@ function removeMember(userId: string) {
} }
function resetCreateGroupForm() { function resetCreateGroupForm() {
createGroupForm.value = { name: '', creatorId: '' } createGroupForm.value = { name: '', creatorId: '', groupType: 'WORK', announcement: '' }
memberSearchKeyword.value = '' memberSearchKeyword.value = ''
memberSearchResults.value = [] memberSearchResults.value = []
selectedMembers.value = [] selectedMembers.value = []
} }
function openCreateUserDialog() {
editingUserId.value = null
registerForm.value = {
userId: '',
nickname: '',
avatar: '',
gender: 'UNKNOWN',
status: 'ACTIVE',
}
showRegisterUser.value = true
}
function openEditUserDialog(user: ImUser) {
editingUserId.value = user.userId
registerForm.value = {
userId: user.userId,
nickname: user.nickname ?? '',
avatar: user.avatar ?? '',
gender: user.gender ?? 'UNKNOWN',
status: user.status ?? 'ACTIVE',
}
showRegisterUser.value = true
}
function resetUserForm() {
editingUserId.value = null
registerForm.value = {
userId: '',
nickname: '',
avatar: '',
gender: 'UNKNOWN',
status: 'ACTIVE',
}
}
function openCreateGroupDialog() {
resetCreateGroupForm()
showCreateGroup.value = true
}
function openEditGroupDialog(group: ImGroup) {
editingGroup.value = group
editGroupForm.value = {
name: group.name,
groupType: (group.groupType || 'WORK') as 'WORK' | 'PUBLIC',
announcement: group.announcement ?? '',
}
showEditGroup.value = true
}
function resetEditGroupForm() {
editingGroup.value = null
editGroupForm.value = {
name: '',
groupType: 'WORK',
announcement: '',
}
}
function resetSendFriendRequestForm() { function resetSendFriendRequestForm() {
sendFriendRequestForm.value = { toUserId: '', remark: '' } sendFriendRequestForm.value = { toUserId: '', remark: '' }
} }
@ -643,9 +684,8 @@ function resetWebhookForm() {
function resetMessageSearch() { function resetMessageSearch() {
historyForm.value = { historyForm.value = {
userA: '',
userB: '',
keyword: '', keyword: '',
chatType: '',
msgType: '', msgType: '',
timeRange: [], timeRange: [],
} }
@ -689,20 +729,17 @@ async function loadGroups() {
} }
async function loadMessages(page = historyPage.value) { async function loadMessages(page = historyPage.value) {
const userA = historyForm.value.userA.trim()
const userB = historyForm.value.userB.trim()
if (!userA || !userB) {
ElMessage.warning('请填写用户A和用户B')
return
}
loadingMessages.value = true loadingMessages.value = true
try { try {
const [startTime, endTime] = historyForm.value.timeRange ?? [] const [startTime, endTime] = historyForm.value.timeRange ?? []
const res = await imAdminApi.getMessages(appId, userA, userB, page, historyPageSize, { const res = await imAdminApi.searchMessages(appId, {
keyword: historyForm.value.keyword.trim() || undefined, keyword: historyForm.value.keyword.trim() || undefined,
chatType: historyForm.value.chatType || undefined,
msgType: historyForm.value.msgType || undefined, msgType: historyForm.value.msgType || undefined,
startTime: startTime ? toLocalDateTime(startTime) : undefined, startTime: startTime ? toLocalDateTime(startTime) : undefined,
endTime: endTime ? toLocalDateTime(endTime) : undefined, endTime: endTime ? toLocalDateTime(endTime) : undefined,
page,
size: historyPageSize,
}) })
messages.value = res.data.data.content messages.value = res.data.data.content
historyTotal.value = res.data.data.totalElements historyTotal.value = res.data.data.totalElements
@ -966,20 +1003,37 @@ async function dismissGroup(group: ImGroup) {
} }
async function submitRegisterUser() { async function submitRegisterUser() {
if (!registerForm.value.userId.trim()) { if (!registerForm.value.userId.trim() && !editingUserId.value) {
ElMessage.warning('请填写用户ID') ElMessage.warning('请填写用户ID')
return return
} }
submittingRegister.value = true submittingRegister.value = true
try { try {
await imAdminApi.registerUser(appId, registerForm.value.userId.trim(), registerForm.value.nickname.trim()) const payload = {
ElMessage.success('用户注册成功') nickname: registerForm.value.nickname.trim() || undefined,
avatar: registerForm.value.avatar.trim() || undefined,
gender: registerForm.value.gender,
status: registerForm.value.status,
}
if (editingUserId.value) {
await imAdminApi.updateUser(appId, editingUserId.value, payload)
ElMessage.success('用户已更新')
} else {
await imAdminApi.registerUser(
appId,
registerForm.value.userId.trim(),
payload.nickname,
payload.avatar,
payload.gender,
payload.status,
)
ElMessage.success('用户注册成功')
}
showRegisterUser.value = false showRegisterUser.value = false
registerForm.value = { userId: '', nickname: '' }
loadUsers() loadUsers()
loadStats() loadStats()
} catch { } catch {
ElMessage.error('注册失败,用户ID可能已存在') ElMessage.error(editingUserId.value ? '更新失败' : '注册失败,用户ID可能已存在')
} finally { } finally {
submittingRegister.value = false submittingRegister.value = false
} }
@ -993,7 +1047,14 @@ async function submitCreateGroup() {
submittingCreateGroup.value = true submittingCreateGroup.value = true
try { try {
const memberIds = selectedMembers.value.map(item => item.userId) const memberIds = selectedMembers.value.map(item => item.userId)
await imAdminApi.createGroup(appId, createGroupForm.value.name.trim(), createGroupForm.value.creatorId.trim(), memberIds) await imAdminApi.createGroup(
appId,
createGroupForm.value.name.trim(),
createGroupForm.value.creatorId.trim(),
memberIds,
createGroupForm.value.groupType,
createGroupForm.value.announcement.trim() || undefined,
)
ElMessage.success('群组创建成功') ElMessage.success('群组创建成功')
showCreateGroup.value = false showCreateGroup.value = false
loadGroups() loadGroups()
@ -1005,13 +1066,32 @@ async function submitCreateGroup() {
} }
} }
async function submitEditGroup() {
if (!editingGroup.value) return
if (!editGroupForm.value.name.trim()) {
ElMessage.warning('请填写群名称')
return
}
submittingEditGroup.value = true
try {
await imAdminApi.updateGroup(appId, editingGroup.value.id, {
name: editGroupForm.value.name.trim(),
groupType: editGroupForm.value.groupType,
announcement: editGroupForm.value.announcement.trim() || undefined,
})
ElMessage.success('群组已更新')
showEditGroup.value = false
loadGroups()
} catch {
ElMessage.error('更新失败')
} finally {
submittingEditGroup.value = false
}
}
function handleTabChange(tab: string) { function handleTabChange(tab: string) {
if (tab === 'groups' && groups.value.length === 0) { if (tab === 'groups' && groups.value.length === 0) {
loadGroups() loadGroups()
} else if (tab === 'friend-requests' && friendRequests.value.length === 0) {
loadFriendRequests()
} else if (tab === 'group-requests' && groupJoinRequests.value.length === 0 && groupRequestGroupId.value.trim()) {
loadGroupJoinRequests()
} else if (tab === 'logs' && operationLogs.value.length === 0) { } else if (tab === 'logs' && operationLogs.value.length === 0) {
loadOperationLogs() loadOperationLogs()
} else if (tab === 'webhooks' && webhooks.value.length === 0) { } else if (tab === 'webhooks' && webhooks.value.length === 0) {
@ -1035,8 +1115,58 @@ function handleOperationLogPageChange(page: number) {
onMounted(() => { onMounted(() => {
loadStats() loadStats()
loadUsers() loadUsers()
loadFriendRequests()
}) })
function logActionLabel(action: string) {
return {
REGISTER_USER: '注册用户',
UPDATE_USER: '更新用户',
UPDATE_USER_STATUS: '调整状态',
CREATE_GROUP: '创建群组',
UPDATE_GROUP: '更新群组',
ADMIN_REVOKE_MESSAGE: '撤回消息',
CREATE_WEBHOOK: '创建回调',
UPDATE_WEBHOOK: '更新回调',
DELETE_WEBHOOK: '删除回调',
CREATE_KEYWORD_FILTER: '创建过滤',
UPDATE_KEYWORD_FILTER: '更新过滤',
DELETE_KEYWORD_FILTER: '删除过滤',
SET_GLOBAL_MUTE: '全局禁言',
VIEW_STATS: '查看统计',
VIEW_HISTORY: '查看消息',
SEARCH_USERS: '搜索用户',
SEARCH_GROUPS: '搜索群组',
SEARCH_MESSAGES: '搜索消息',
}[action] ?? action
}
function logResourceLabel(resourceType: string) {
return {
ACCOUNT: '用户',
GROUP: '群组',
MESSAGE: '消息',
WEBHOOK: '回调',
KEYWORD_FILTER: '过滤',
GLOBAL_MUTE: '全局禁言',
STATS: '统计',
}[resourceType] ?? resourceType
}
function formatLogDetail(detail?: string | null) {
if (!detail) return '-'
const trimmed = detail.trim()
if (!trimmed) return '-'
try {
const parsed = JSON.parse(trimmed)
return typeof parsed === 'object' ? JSON.stringify(parsed, null, 2) : String(parsed)
} catch {
return trimmed
}
}
function userAvatarFallback(row: ImUser) {
return (row.nickname || row.userId || '?').slice(0, 1).toUpperCase()
}
</script> </script>
<style scoped> <style scoped>
@ -1128,4 +1258,10 @@ onMounted(() => {
max-width: 100%; max-width: 100%;
word-break: break-all; word-break: break-all;
} }
.log-detail {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
</style> </style>