feat(sdk): 添加即时通讯和推送功能

- 新增 ApiClient 类用于处理 API 请求和响应
- 实现 ImClient 类支持 WebSocket 连接和消息收发
- 添加 ImSDK 类提供完整的即时通讯功能接口
- 定义 ImTypes.swift 包含聊天类型、消息类型等相关数据结构
- 实现 PushSDK 类支持推送通知令牌注册
- 添加基础的 UpdateSDK 框架结构
- 集成登录认证和聊天室订阅功能
- 实现群组管理、好友关系和会话功能
- 支持多种消息类型包括文本、图片、视频、音频等
- 提供历史消息查询和黑名单管理功能
这个提交包含在:
XuqmGroup 2026-04-28 10:27:24 +08:00
父节点 491a1ce8d3
当前提交 a6920641ad
共有 2 个文件被更改,包括 410 次插入81 次删除

查看文件

@ -19,7 +19,7 @@ export interface ImUser {
avatar?: string avatar?: string
status: 'ACTIVE' | 'BANNED' status: 'ACTIVE' | 'BANNED'
gender: 'UNKNOWN' | 'MALE' | 'FEMALE' gender: 'UNKNOWN' | 'MALE' | 'FEMALE'
createdAt: string createdAt: number
} }
export interface ImGroup { export interface ImGroup {
@ -27,9 +27,33 @@ export interface ImGroup {
appId: string appId: string
name: string name: string
creatorId: string creatorId: string
memberIds: string[] groupType?: string | null
adminIds: string[] memberIds: string
createdAt: string adminIds: string
announcement?: string | null
createdAt: number
}
export interface ImMessage {
id: string
appId: string
fromUserId: string
toId: string
chatType: 'SINGLE' | 'GROUP'
msgType: string
content: string
status: string
mentionedUserIds?: string | null
groupReadCount?: number | null
createdAt: number
}
export interface PagedResult<T> {
content: T[]
totalElements: number
totalPages: number
size: number
number: number
} }
export interface ImStats { export interface ImStats {
@ -41,13 +65,13 @@ export interface ImStats {
export const imAdminApi = { export const imAdminApi = {
listUsers(appId: string, page = 0, size = 20) { listUsers(appId: string, page = 0, size = 20) {
return imClient.get<{ data: { content: ImUser[]; totalElements: number; totalPages: number } }>( return imClient.get<{ data: PagedResult<ImUser> }>(
'/api/im/admin/users', { params: { appId, page, size } }, '/api/im/admin/users', { params: { appId, page, size } },
) )
}, },
updateUserStatus(appId: string, userId: string, status: 'ACTIVE' | 'BANNED') { updateUserStatus(appId: string, userId: string, status: 'ACTIVE' | 'BANNED') {
return imClient.put(`/api/im/admin/users/${userId}/status`, { status }, { params: { appId } }) return imClient.put(`/api/im/admin/users/${encodeURIComponent(userId)}/status`, { status }, { params: { appId } })
}, },
listGroups(appId: string) { listGroups(appId: string) {
@ -58,6 +82,46 @@ export const imAdminApi = {
return imClient.get<{ data: ImStats }>('/api/im/admin/stats', { params: { appId } }) return imClient.get<{ data: ImStats }>('/api/im/admin/stats', { params: { appId } })
}, },
getMessages(
appId: string,
userA: string,
userB: string,
page = 0,
size = 20,
filters?: {
msgType?: string
keyword?: string
startTime?: string
endTime?: string
},
) {
return imClient.get<{ data: PagedResult<ImMessage> }>('/api/im/admin/messages', {
params: {
appId,
userA,
userB,
page,
size,
...(filters?.msgType ? { msgType: filters.msgType } : {}),
...(filters?.keyword ? { keyword: filters.keyword } : {}),
...(filters?.startTime ? { startTime: filters.startTime } : {}),
...(filters?.endTime ? { endTime: filters.endTime } : {}),
},
})
},
revokeMessage(appId: string, messageId: string) {
return imClient.post<{ data: ImMessage }>(
`/api/im/admin/messages/${encodeURIComponent(messageId)}/revoke`,
{},
{ params: { appId } },
)
},
dismissGroup(appId: string, groupId: string) {
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) {
return imClient.post<{ data: ImUser }>( return imClient.post<{ data: ImUser }>(
'/api/im/admin/users', '/api/im/admin/users',

查看文件

@ -2,7 +2,6 @@
<div> <div>
<el-page-header @back="$router.back()" :content="`即时通讯管理 — ${appId}`" style="margin-bottom:20px" /> <el-page-header @back="$router.back()" :content="`即时通讯管理 — ${appId}`" style="margin-bottom:20px" />
<!-- Stats -->
<el-row :gutter="16" style="margin-bottom:20px"> <el-row :gutter="16" style="margin-bottom:20px">
<el-col :span="6" v-for="item in statCards" :key="item.label"> <el-col :span="6" v-for="item in statCards" :key="item.label">
<el-card shadow="never"> <el-card shadow="never">
@ -14,44 +13,48 @@
</el-col> </el-col>
</el-row> </el-row>
<el-card> <el-card shadow="never">
<el-tabs v-model="activeTab" @tab-change="handleTabChange"> <el-tabs v-model="activeTab" @tab-change="handleTabChange">
<!-- Users Tab -->
<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="showRegisterUser = true">注册用户</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="160" /> <el-table-column prop="userId" label="用户ID" width="180" />
<el-table-column prop="nickname" label="昵称" width="120" /> <el-table-column prop="nickname" label="昵称" width="140" />
<el-table-column prop="gender" label="性别" width="80"> <el-table-column prop="gender" label="性别" width="90">
<template #default="{row}"> <template #default="{ row }">
{{ { UNKNOWN: '未知', MALE: '男', FEMALE: '女' }[row.gender] }} {{ genderLabel[row.gender] ?? '未知' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" width="100"> <el-table-column prop="status" label="状态" width="100">
<template #default="{row}"> <template #default="{ row }">
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'danger'" size="small"> <el-tag :type="row.status === 'ACTIVE' ? 'success' : 'danger'" size="small">
{{ row.status === 'ACTIVE' ? '正常' : '封禁' }} {{ row.status === 'ACTIVE' ? '正常' : '封禁' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<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="140" fixed="right">
<template #default="{row}"> <template #default="{ row }">
<el-button <el-button
link link
:type="row.status === 'ACTIVE' ? 'danger' : 'success'" :type="row.status === 'ACTIVE' ? 'danger' : 'success'"
size="small" size="small"
@click="toggleUserStatus(row)"> @click="toggleUserStatus(row)"
>
{{ row.status === 'ACTIVE' ? '封禁' : '解封' }} {{ row.status === 'ACTIVE' ? '封禁' : '解封' }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
v-if="userTotal > userPageSize" v-if="userTotal > userPageSize"
style="margin-top:16px" style="margin-top:16px"
@ -59,36 +62,125 @@
:total="userTotal" :total="userTotal"
:page-size="userPageSize" :page-size="userPageSize"
:current-page="userPage + 1" :current-page="userPage + 1"
@current-change="p => { userPage = p - 1; loadUsers() }" @current-change="handleUserPageChange"
/> />
</el-tab-pane> </el-tab-pane>
<!-- Groups Tab -->
<el-tab-pane label="群组列表" name="groups"> <el-tab-pane label="群组列表" name="groups">
<div class="toolbar"> <div class="toolbar">
<el-button type="primary" @click="showCreateGroup = true">创建群组</el-button>
<el-button @click="loadGroups" :loading="loadingGroups">刷新</el-button> <el-button @click="loadGroups" :loading="loadingGroups">刷新</el-button>
</div> </div>
<el-table :data="groups" v-loading="loadingGroups" border stripe> <el-table :data="groups" v-loading="loadingGroups" border stripe>
<el-table-column prop="id" label="群组ID" width="240" /> <el-table-column prop="id" label="群组ID" width="240" />
<el-table-column prop="name" label="群名称" /> <el-table-column prop="name" label="群名称" min-width="160" />
<el-table-column prop="creatorId" label="创建者" width="160" /> <el-table-column prop="groupType" label="类型" width="110">
<el-table-column label="成员数" width="100"> <template #default="{ row }">
<template #default="{row}">{{ (row.memberIds ?? []).length }}</template> {{ row.groupType || 'WORK' }}
</template>
</el-table-column> </el-table-column>
<el-table-column label="成员数" width="100">
<template #default="{ row }">{{ parseIdList(row.memberIds).length }}</template>
</el-table-column>
<el-table-column label="管理员" width="100">
<template #default="{ row }">{{ parseIdList(row.adminIds).length }}</template>
</el-table-column>
<el-table-column prop="creatorId" label="群主" width="160" />
<el-table-column prop="announcement" label="公告" min-width="200" show-overflow-tooltip />
<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 label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link type="danger" size="small" @click="dismissGroup(row)">
解散
</el-button>
</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="消息历史" name="messages">
<el-form :inline="true" :model="historyForm" class="toolbar" label-width="0">
<el-form-item>
<el-input v-model="historyForm.userA" placeholder="用户A" clearable style="width:160px" />
</el-form-item>
<el-form-item>
<el-input v-model="historyForm.userB" placeholder="用户B" clearable style="width:160px" />
</el-form-item>
<el-form-item>
<el-input v-model="historyForm.keyword" placeholder="关键词" clearable style="width:180px" />
</el-form-item>
<el-form-item>
<el-select v-model="historyForm.msgType" placeholder="消息类型" clearable style="width:140px">
<el-option v-for="item in msgTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-date-picker
v-model="historyForm.timeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width:340px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loadingMessages" @click="searchMessages">
查询
</el-button>
</el-form-item>
<el-form-item>
<el-button @click="resetMessageSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="messages" v-loading="loadingMessages" 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="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="status" label="状态" width="100" />
<el-table-column prop="content" label="内容" min-width="260">
<template #default="{ row }">
<span class="content-preview">{{ formatContent(row.content) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ row }">
<el-button link type="danger" size="small" @click="revokeMessage(row)">
撤回
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="historyTotal > historyPageSize"
style="margin-top:16px"
layout="prev, pager, next"
:total="historyTotal"
:page-size="historyPageSize"
:current-page="historyPage + 1"
@current-change="handleHistoryPageChange"
/>
</el-tab-pane>
</el-tabs> </el-tabs>
</el-card> </el-card>
<!-- Register User Dialog --> <el-dialog v-model="showRegisterUser" title="注册用户" width="420px">
<el-dialog v-model="showRegisterUser" title="注册用户" width="400px"> <el-form :model="registerForm" label-width="72px">
<el-form :model="registerForm" label-width="80px"> <el-form-item label="用户ID">
<el-form-item label="用户ID"><el-input v-model="registerForm.userId" placeholder="全局唯一标识" /></el-form-item> <el-input v-model="registerForm.userId" placeholder="全局唯一标识" />
<el-form-item label="昵称"><el-input v-model="registerForm.nickname" /></el-form-item> </el-form-item>
<el-form-item label="昵称">
<el-input v-model="registerForm.nickname" />
</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>
@ -96,11 +188,14 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- Create Group Dialog --> <el-dialog v-model="showCreateGroup" title="创建群组" width="520px" @closed="resetCreateGroupForm">
<el-dialog v-model="showCreateGroup" title="创建群组" width="480px" @closed="resetCreateGroupForm">
<el-form :model="createGroupForm" label-width="90px"> <el-form :model="createGroupForm" label-width="90px">
<el-form-item label="群名称"><el-input v-model="createGroupForm.name" /></el-form-item> <el-form-item label="群名称">
<el-form-item label="创建者ID"><el-input v-model="createGroupForm.creatorId" /></el-form-item> <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-form-item label="搜索成员">
<el-input <el-input
v-model="memberSearchKeyword" v-model="memberSearchKeyword"
@ -109,7 +204,6 @@
clearable clearable
@input="onMemberSearchInput" @input="onMemberSearchInput"
/> />
<!-- Search results list -->
<div v-if="memberSearchResults.length > 0" class="search-results"> <div v-if="memberSearchResults.length > 0" class="search-results">
<div <div
v-for="user in memberSearchResults" v-for="user in memberSearchResults"
@ -121,8 +215,12 @@
<span class="result-userid">{{ user.userId }}</span> <span class="result-userid">{{ user.userId }}</span>
</div> </div>
</div> </div>
<el-empty v-else-if="memberSearchKeyword && !searchingMembers && memberSearchResults.length === 0" <el-empty
description="未找到匹配用户" :image-size="48" style="padding:8px 0" /> v-else-if="memberSearchKeyword && !searchingMembers && memberSearchResults.length === 0"
description="未找到匹配用户"
:image-size="48"
style="padding:8px 0"
/>
</el-form-item> </el-form-item>
<el-form-item label="已选成员"> <el-form-item label="已选成员">
<div class="selected-members"> <div class="selected-members">
@ -149,15 +247,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import { imAdminApi, type ImUser, type ImGroup, type ImStats } from '@/api/im' import { imAdminApi, type ImGroup, type ImMessage, type ImStats, type ImUser } from '@/api/im'
const route = useRoute() const route = useRoute()
const appId = route.params.appId as string const appId = route.params.appId as string
const genderLabel: Record<string, string> = {
UNKNOWN: '未知',
MALE: '男',
FEMALE: '女',
}
const msgTypeOptions = [
'TEXT', 'IMAGE', 'VIDEO', 'AUDIO', 'FILE', 'CUSTOM', 'LOCATION', 'NOTIFY',
'RICH_TEXT', 'CALL_AUDIO', 'CALL_VIDEO', 'FORWARD', 'QUOTE', 'MERGE', 'REVOKED',
].map(value => ({ label: value, value }))
const activeTab = ref('users') const activeTab = ref('users')
const stats = ref<ImStats | null>(null) const stats = ref<ImStats | null>(null)
@ -170,6 +279,20 @@ const userTotal = ref(0)
const groups = ref<ImGroup[]>([]) const groups = ref<ImGroup[]>([])
const loadingGroups = ref(false) const loadingGroups = ref(false)
const messages = ref<ImMessage[]>([])
const loadingMessages = ref(false)
const historyPage = ref(0)
const historyPageSize = 20
const historyTotal = ref(0)
const historyForm = ref({
userA: '',
userB: '',
keyword: '',
msgType: '',
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 registerForm = ref({ userId: '', nickname: '' })
@ -178,27 +301,58 @@ const showCreateGroup = ref(false)
const submittingCreateGroup = ref(false) const submittingCreateGroup = ref(false)
const createGroupForm = ref({ name: '', creatorId: '' }) const createGroupForm = ref({ name: '', creatorId: '' })
// Member fuzzy search
const memberSearchKeyword = ref('') const memberSearchKeyword = ref('')
const memberSearchResults = ref<ImUser[]>([]) const memberSearchResults = ref<ImUser[]>([])
const searchingMembers = ref(false) const searchingMembers = ref(false)
const selectedMembers = ref<ImUser[]>([]) const selectedMembers = ref<ImUser[]>([])
let memberSearchTimer: ReturnType<typeof setTimeout> | null = null let memberSearchTimer: ReturnType<typeof setTimeout> | null = null
const statCards = computed(() => [
{ label: '注册用户', value: stats.value?.totalUsers ?? '-' },
{ label: '群组数', value: stats.value?.totalGroups ?? '-' },
{ label: '消息总量', value: stats.value?.totalMessages ?? '-' },
{ label: '今日消息', value: stats.value?.todayMessages ?? '-' },
])
function formatTime(value: number | string | null | undefined) {
if (value === null || value === undefined || value === '') return '-'
const date = new Date(value)
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString('zh-CN')
}
function formatContent(content: string) {
if (!content) return '-'
return content.length > 120 ? `${content.slice(0, 120)}...` : content
}
function parseIdList(value: string) {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
function toLocalDateTime(value: string) {
if (!value) return ''
if (value.includes('T')) return value
return value.replace(' ', 'T')
}
function onMemberSearchInput() { function onMemberSearchInput() {
if (memberSearchTimer) clearTimeout(memberSearchTimer) if (memberSearchTimer) clearTimeout(memberSearchTimer)
const kw = memberSearchKeyword.value.trim() const keyword = memberSearchKeyword.value.trim()
if (!kw) { if (!keyword) {
memberSearchResults.value = [] memberSearchResults.value = []
return return
} }
memberSearchTimer = setTimeout(async () => { memberSearchTimer = setTimeout(async () => {
searchingMembers.value = true searchingMembers.value = true
try { try {
const res = await imAdminApi.searchUsers(appId, kw) const res = await imAdminApi.searchUsers(appId, keyword)
// Exclude already-selected members from results const selectedIds = new Set(selectedMembers.value.map(item => item.userId))
const selectedIds = new Set(selectedMembers.value.map(m => m.userId)) memberSearchResults.value = res.data.data.filter(user => !selectedIds.has(user.userId))
memberSearchResults.value = res.data.data.filter(u => !selectedIds.has(u.userId))
} catch { } catch {
memberSearchResults.value = [] memberSearchResults.value = []
} finally { } finally {
@ -208,15 +362,15 @@ function onMemberSearchInput() {
} }
function addMember(user: ImUser) { function addMember(user: ImUser) {
if (!selectedMembers.value.find(m => m.userId === user.userId)) { if (!selectedMembers.value.find(item => item.userId === user.userId)) {
selectedMembers.value.push(user) selectedMembers.value.push(user)
} }
memberSearchResults.value = memberSearchResults.value.filter(u => u.userId !== user.userId) memberSearchResults.value = memberSearchResults.value.filter(item => item.userId !== user.userId)
memberSearchKeyword.value = '' memberSearchKeyword.value = ''
} }
function removeMember(userId: string) { function removeMember(userId: string) {
selectedMembers.value = selectedMembers.value.filter(m => m.userId !== userId) selectedMembers.value = selectedMembers.value.filter(item => item.userId !== userId)
} }
function resetCreateGroupForm() { function resetCreateGroupForm() {
@ -226,22 +380,26 @@ function resetCreateGroupForm() {
selectedMembers.value = [] selectedMembers.value = []
} }
const statCards = computed(() => [ function resetMessageSearch() {
{ label: '注册用户', value: stats.value?.totalUsers ?? '-' }, historyForm.value = {
{ label: '群组数', value: stats.value?.totalGroups ?? '-' }, userA: '',
{ label: '消息总量', value: stats.value?.totalMessages ?? '-' }, userB: '',
{ label: '今日消息', value: stats.value?.todayMessages ?? '-' }, keyword: '',
]) msgType: '',
timeRange: [],
function formatTime(t: string) { }
return t ? new Date(t).toLocaleString('zh-CN') : '-' messages.value = []
historyTotal.value = 0
historyPage.value = 0
} }
async function loadStats() { async function loadStats() {
try { try {
const res = await imAdminApi.getStats(appId) const res = await imAdminApi.getStats(appId)
stats.value = res.data.data stats.value = res.data.data
} catch {} } catch {
stats.value = null
}
} }
async function loadUsers() { async function loadUsers() {
@ -250,7 +408,7 @@ async function loadUsers() {
const res = await imAdminApi.listUsers(appId, userPage.value, userPageSize) const res = await imAdminApi.listUsers(appId, userPage.value, userPageSize)
users.value = res.data.data.content users.value = res.data.data.content
userTotal.value = res.data.data.totalElements userTotal.value = res.data.data.totalElements
} catch (e) { } catch {
ElMessage.error('加载用户失败') ElMessage.error('加载用户失败')
} finally { } finally {
loadingUsers.value = false loadingUsers.value = false
@ -262,35 +420,86 @@ async function loadGroups() {
try { try {
const res = await imAdminApi.listGroups(appId) const res = await imAdminApi.listGroups(appId)
groups.value = res.data.data groups.value = res.data.data
} catch (e) { } catch {
ElMessage.error('加载群组失败') ElMessage.error('加载群组失败')
} finally { } finally {
loadingGroups.value = false loadingGroups.value = false
} }
} }
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
try {
const [startTime, endTime] = historyForm.value.timeRange ?? []
const res = await imAdminApi.getMessages(appId, userA, userB, page, historyPageSize, {
keyword: historyForm.value.keyword.trim() || undefined,
msgType: historyForm.value.msgType || undefined,
startTime: startTime ? toLocalDateTime(startTime) : undefined,
endTime: endTime ? toLocalDateTime(endTime) : undefined,
})
messages.value = res.data.data.content
historyTotal.value = res.data.data.totalElements
historyPage.value = page
} catch {
ElMessage.error('加载消息历史失败')
} finally {
loadingMessages.value = false
}
}
async function searchMessages() {
await loadMessages(0)
}
async function toggleUserStatus(user: ImUser) { async function toggleUserStatus(user: ImUser) {
const newStatus = user.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE' const nextStatus = user.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE'
const action = newStatus === 'BANNED' ? '封禁' : '解封' const action = nextStatus === 'BANNED' ? '封禁' : '解封'
await ElMessageBox.confirm(`确认${action}用户 ${user.userId}`, `${action}用户`, { await ElMessageBox.confirm(`确认${action}用户 ${user.userId}`, `${action}用户`, {
type: 'warning', type: 'warning',
confirmButtonText: '确认', confirmButtonText: '确认',
cancelButtonText: '取消', cancelButtonText: '取消',
}) })
await imAdminApi.updateUserStatus(appId, user.userId, newStatus) await imAdminApi.updateUserStatus(appId, user.userId, nextStatus)
ElMessage.success(`${action}`) ElMessage.success(`${action}`)
loadUsers() loadUsers()
} }
function handleTabChange(tab: string) { async function revokeMessage(message: ImMessage) {
if (tab === 'groups' && groups.value.length === 0) loadGroups() await ElMessageBox.confirm(`确认撤回消息 ${message.id}`, '撤回消息', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消',
})
await imAdminApi.revokeMessage(appId, message.id)
ElMessage.success('消息已撤回')
loadMessages(historyPage.value)
}
async function dismissGroup(group: ImGroup) {
await ElMessageBox.confirm(`确认解散群组 ${group.name}`, '解散群组', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消',
})
await imAdminApi.dismissGroup(appId, group.id)
ElMessage.success('群组已解散')
loadGroups()
} }
async function submitRegisterUser() { async function submitRegisterUser() {
if (!registerForm.value.userId.trim()) return ElMessage.warning('请填写用户ID') if (!registerForm.value.userId.trim()) {
ElMessage.warning('请填写用户ID')
return
}
submittingRegister.value = true submittingRegister.value = true
try { try {
await imAdminApi.registerUser(appId, registerForm.value.userId, registerForm.value.nickname) await imAdminApi.registerUser(appId, registerForm.value.userId.trim(), registerForm.value.nickname.trim())
ElMessage.success('用户注册成功') ElMessage.success('用户注册成功')
showRegisterUser.value = false showRegisterUser.value = false
registerForm.value = { userId: '', nickname: '' } registerForm.value = { userId: '', nickname: '' }
@ -305,12 +514,13 @@ async function submitRegisterUser() {
async function submitCreateGroup() { async function submitCreateGroup() {
if (!createGroupForm.value.name.trim() || !createGroupForm.value.creatorId.trim()) { if (!createGroupForm.value.name.trim() || !createGroupForm.value.creatorId.trim()) {
return ElMessage.warning('请填写群名称和创建者ID') ElMessage.warning('请填写群名称和创建者ID')
return
} }
submittingCreateGroup.value = true submittingCreateGroup.value = true
try { try {
const memberIds = selectedMembers.value.map(m => m.userId) const memberIds = selectedMembers.value.map(item => item.userId)
await imAdminApi.createGroup(appId, createGroupForm.value.name, createGroupForm.value.creatorId, memberIds) await imAdminApi.createGroup(appId, createGroupForm.value.name.trim(), createGroupForm.value.creatorId.trim(), memberIds)
ElMessage.success('群组创建成功') ElMessage.success('群组创建成功')
showCreateGroup.value = false showCreateGroup.value = false
loadGroups() loadGroups()
@ -322,6 +532,21 @@ async function submitCreateGroup() {
} }
} }
function handleTabChange(tab: string) {
if (tab === 'groups' && groups.value.length === 0) {
loadGroups()
}
}
function handleUserPageChange(page: number) {
userPage.value = page - 1
loadUsers()
}
function handleHistoryPageChange(page: number) {
loadMessages(page - 1)
}
onMounted(() => { onMounted(() => {
loadStats() loadStats()
loadUsers() loadUsers()
@ -329,10 +554,28 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.stat-card { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px 0; } .stat-card {
.stat-value { font-size: 28px; font-weight: 800; color: #1f2933; } display: flex;
.stat-label { font-size: 13px; color: #6b7280; } flex-direction: column;
.toolbar { margin-bottom: 12px; } align-items: center;
gap: 4px;
padding: 8px 0;
}
.stat-value {
font-size: 28px;
font-weight: 800;
color: #1f2933;
}
.stat-label {
font-size: 13px;
color: #6b7280;
}
.toolbar {
margin-bottom: 12px;
}
.search-results { .search-results {
width: 100%; width: 100%;
@ -343,6 +586,7 @@ onMounted(() => {
overflow-y: auto; overflow-y: auto;
background: #fff; background: #fff;
} }
.search-result-item { .search-result-item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -351,9 +595,20 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: background 0.15s;
} }
.search-result-item:hover { background: #f0f4ff; }
.result-nickname { font-weight: 500; color: #303133; } .search-result-item:hover {
.result-userid { font-size: 12px; color: #909399; } background: #f0f4ff;
}
.result-nickname {
font-weight: 500;
color: #303133;
}
.result-userid {
font-size: 12px;
color: #909399;
}
.selected-members { .selected-members {
display: flex; display: flex;
@ -362,5 +617,15 @@ onMounted(() => {
min-height: 32px; min-height: 32px;
width: 100%; width: 100%;
} }
.no-members-hint { font-size: 13px; color: #c0c4cc; }
.no-members-hint {
font-size: 13px;
color: #c0c4cc;
}
.content-preview {
display: inline-block;
max-width: 100%;
word-break: break-all;
}
</style> </style>