feat(sdk): 添加即时通讯和推送功能
- 新增 ApiClient 类用于处理 API 请求和响应 - 实现 ImClient 类支持 WebSocket 连接和消息收发 - 添加 ImSDK 类提供完整的即时通讯功能接口 - 定义 ImTypes.swift 包含聊天类型、消息类型等相关数据结构 - 实现 PushSDK 类支持推送通知令牌注册 - 添加基础的 UpdateSDK 框架结构 - 集成登录认证和聊天室订阅功能 - 实现群组管理、好友关系和会话功能 - 支持多种消息类型包括文本、图片、视频、音频等 - 提供历史消息查询和黑名单管理功能
这个提交包含在:
父节点
491a1ce8d3
当前提交
a6920641ad
@ -19,7 +19,7 @@ export interface ImUser {
|
||||
avatar?: string
|
||||
status: 'ACTIVE' | 'BANNED'
|
||||
gender: 'UNKNOWN' | 'MALE' | 'FEMALE'
|
||||
createdAt: string
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export interface ImGroup {
|
||||
@ -27,9 +27,33 @@ export interface ImGroup {
|
||||
appId: string
|
||||
name: string
|
||||
creatorId: string
|
||||
memberIds: string[]
|
||||
adminIds: string[]
|
||||
createdAt: string
|
||||
groupType?: string | null
|
||||
memberIds: 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 {
|
||||
@ -41,13 +65,13 @@ export interface ImStats {
|
||||
|
||||
export const imAdminApi = {
|
||||
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 } },
|
||||
)
|
||||
},
|
||||
|
||||
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) {
|
||||
@ -58,6 +82,46 @@ export const imAdminApi = {
|
||||
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) {
|
||||
return imClient.post<{ data: ImUser }>(
|
||||
'/api/im/admin/users',
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
<div>
|
||||
<el-page-header @back="$router.back()" :content="`即时通讯管理 — ${appId}`" style="margin-bottom:20px" />
|
||||
|
||||
<!-- Stats -->
|
||||
<el-row :gutter="16" style="margin-bottom:20px">
|
||||
<el-col :span="6" v-for="item in statCards" :key="item.label">
|
||||
<el-card shadow="never">
|
||||
@ -14,44 +13,48 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card>
|
||||
<el-card shadow="never">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<!-- Users Tab -->
|
||||
<el-tab-pane label="注册用户" name="users">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="showRegisterUser = true">注册用户</el-button>
|
||||
<el-button @click="loadUsers" :loading="loadingUsers">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="users" v-loading="loadingUsers" border stripe>
|
||||
<el-table-column prop="userId" label="用户ID" width="160" />
|
||||
<el-table-column prop="nickname" label="昵称" width="120" />
|
||||
<el-table-column prop="gender" label="性别" width="80">
|
||||
<template #default="{row}">
|
||||
{{ { UNKNOWN: '未知', MALE: '男', FEMALE: '女' }[row.gender] }}
|
||||
<el-table-column prop="userId" label="用户ID" width="180" />
|
||||
<el-table-column prop="nickname" label="昵称" width="140" />
|
||||
<el-table-column prop="gender" label="性别" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ genderLabel[row.gender] ?? '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{row}">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 'ACTIVE' ? '正常' : '封禁' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
:type="row.status === 'ACTIVE' ? 'danger' : 'success'"
|
||||
size="small"
|
||||
@click="toggleUserStatus(row)">
|
||||
@click="toggleUserStatus(row)"
|
||||
>
|
||||
{{ row.status === 'ACTIVE' ? '封禁' : '解封' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-if="userTotal > userPageSize"
|
||||
style="margin-top:16px"
|
||||
@ -59,36 +62,125 @@
|
||||
:total="userTotal"
|
||||
:page-size="userPageSize"
|
||||
:current-page="userPage + 1"
|
||||
@current-change="p => { userPage = p - 1; loadUsers() }"
|
||||
@current-change="handleUserPageChange"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Groups Tab -->
|
||||
<el-tab-pane label="群组列表" name="groups">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="showCreateGroup = true">创建群组</el-button>
|
||||
<el-button @click="loadGroups" :loading="loadingGroups">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="groups" v-loading="loadingGroups" border stripe>
|
||||
<el-table-column prop="id" label="群组ID" width="240" />
|
||||
<el-table-column prop="name" label="群名称" />
|
||||
<el-table-column prop="creatorId" label="创建者" width="160" />
|
||||
<el-table-column label="成员数" width="100">
|
||||
<template #default="{row}">{{ (row.memberIds ?? []).length }}</template>
|
||||
<el-table-column prop="name" label="群名称" min-width="160" />
|
||||
<el-table-column prop="groupType" label="类型" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ row.groupType || 'WORK' }}
|
||||
</template>
|
||||
</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">
|
||||
<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>
|
||||
</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-card>
|
||||
|
||||
<!-- Register User Dialog -->
|
||||
<el-dialog v-model="showRegisterUser" title="注册用户" width="400px">
|
||||
<el-form :model="registerForm" label-width="80px">
|
||||
<el-form-item label="用户ID"><el-input v-model="registerForm.userId" placeholder="全局唯一标识" /></el-form-item>
|
||||
<el-form-item label="昵称"><el-input v-model="registerForm.nickname" /></el-form-item>
|
||||
<el-dialog v-model="showRegisterUser" title="注册用户" width="420px">
|
||||
<el-form :model="registerForm" label-width="72px">
|
||||
<el-form-item label="用户ID">
|
||||
<el-input v-model="registerForm.userId" placeholder="全局唯一标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="registerForm.nickname" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showRegisterUser = false">取消</el-button>
|
||||
@ -96,11 +188,14 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Create Group Dialog -->
|
||||
<el-dialog v-model="showCreateGroup" title="创建群组" width="480px" @closed="resetCreateGroupForm">
|
||||
<el-dialog v-model="showCreateGroup" title="创建群组" width="520px" @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.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="memberSearchKeyword"
|
||||
@ -109,7 +204,6 @@
|
||||
clearable
|
||||
@input="onMemberSearchInput"
|
||||
/>
|
||||
<!-- Search results list -->
|
||||
<div v-if="memberSearchResults.length > 0" class="search-results">
|
||||
<div
|
||||
v-for="user in memberSearchResults"
|
||||
@ -121,8 +215,12 @@
|
||||
<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-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">
|
||||
@ -149,15 +247,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } 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'
|
||||
import { imAdminApi, type ImGroup, type ImMessage, type ImStats, type ImUser } from '@/api/im'
|
||||
|
||||
const route = useRoute()
|
||||
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 stats = ref<ImStats | null>(null)
|
||||
|
||||
@ -170,6 +279,20 @@ const userTotal = ref(0)
|
||||
const groups = ref<ImGroup[]>([])
|
||||
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 submittingRegister = ref(false)
|
||||
const registerForm = ref({ userId: '', nickname: '' })
|
||||
@ -178,27 +301,58 @@ const showCreateGroup = ref(false)
|
||||
const submittingCreateGroup = ref(false)
|
||||
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
|
||||
|
||||
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() {
|
||||
if (memberSearchTimer) clearTimeout(memberSearchTimer)
|
||||
const kw = memberSearchKeyword.value.trim()
|
||||
if (!kw) {
|
||||
const keyword = memberSearchKeyword.value.trim()
|
||||
if (!keyword) {
|
||||
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))
|
||||
const res = await imAdminApi.searchUsers(appId, keyword)
|
||||
const selectedIds = new Set(selectedMembers.value.map(item => item.userId))
|
||||
memberSearchResults.value = res.data.data.filter(user => !selectedIds.has(user.userId))
|
||||
} catch {
|
||||
memberSearchResults.value = []
|
||||
} finally {
|
||||
@ -208,15 +362,15 @@ function onMemberSearchInput() {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
memberSearchResults.value = memberSearchResults.value.filter(u => u.userId !== user.userId)
|
||||
memberSearchResults.value = memberSearchResults.value.filter(item => item.userId !== user.userId)
|
||||
memberSearchKeyword.value = ''
|
||||
}
|
||||
|
||||
function removeMember(userId: string) {
|
||||
selectedMembers.value = selectedMembers.value.filter(m => m.userId !== userId)
|
||||
selectedMembers.value = selectedMembers.value.filter(item => item.userId !== userId)
|
||||
}
|
||||
|
||||
function resetCreateGroupForm() {
|
||||
@ -226,22 +380,26 @@ function resetCreateGroupForm() {
|
||||
selectedMembers.value = []
|
||||
}
|
||||
|
||||
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(t: string) {
|
||||
return t ? new Date(t).toLocaleString('zh-CN') : '-'
|
||||
function resetMessageSearch() {
|
||||
historyForm.value = {
|
||||
userA: '',
|
||||
userB: '',
|
||||
keyword: '',
|
||||
msgType: '',
|
||||
timeRange: [],
|
||||
}
|
||||
messages.value = []
|
||||
historyTotal.value = 0
|
||||
historyPage.value = 0
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await imAdminApi.getStats(appId)
|
||||
stats.value = res.data.data
|
||||
} catch {}
|
||||
} catch {
|
||||
stats.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
@ -250,7 +408,7 @@ async function loadUsers() {
|
||||
const res = await imAdminApi.listUsers(appId, userPage.value, userPageSize)
|
||||
users.value = res.data.data.content
|
||||
userTotal.value = res.data.data.totalElements
|
||||
} catch (e) {
|
||||
} catch {
|
||||
ElMessage.error('加载用户失败')
|
||||
} finally {
|
||||
loadingUsers.value = false
|
||||
@ -262,35 +420,86 @@ async function loadGroups() {
|
||||
try {
|
||||
const res = await imAdminApi.listGroups(appId)
|
||||
groups.value = res.data.data
|
||||
} catch (e) {
|
||||
} catch {
|
||||
ElMessage.error('加载群组失败')
|
||||
} finally {
|
||||
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) {
|
||||
const newStatus = user.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE'
|
||||
const action = newStatus === 'BANNED' ? '封禁' : '解封'
|
||||
const nextStatus = user.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE'
|
||||
const action = nextStatus === 'BANNED' ? '封禁' : '解封'
|
||||
await ElMessageBox.confirm(`确认${action}用户 ${user.userId}?`, `${action}用户`, {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
await imAdminApi.updateUserStatus(appId, user.userId, newStatus)
|
||||
await imAdminApi.updateUserStatus(appId, user.userId, nextStatus)
|
||||
ElMessage.success(`已${action}`)
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
function handleTabChange(tab: string) {
|
||||
if (tab === 'groups' && groups.value.length === 0) loadGroups()
|
||||
async function revokeMessage(message: ImMessage) {
|
||||
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() {
|
||||
if (!registerForm.value.userId.trim()) return ElMessage.warning('请填写用户ID')
|
||||
if (!registerForm.value.userId.trim()) {
|
||||
ElMessage.warning('请填写用户ID')
|
||||
return
|
||||
}
|
||||
submittingRegister.value = true
|
||||
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('用户注册成功')
|
||||
showRegisterUser.value = false
|
||||
registerForm.value = { userId: '', nickname: '' }
|
||||
@ -305,12 +514,13 @@ async function submitRegisterUser() {
|
||||
|
||||
async function submitCreateGroup() {
|
||||
if (!createGroupForm.value.name.trim() || !createGroupForm.value.creatorId.trim()) {
|
||||
return ElMessage.warning('请填写群名称和创建者ID')
|
||||
ElMessage.warning('请填写群名称和创建者ID')
|
||||
return
|
||||
}
|
||||
submittingCreateGroup.value = true
|
||||
try {
|
||||
const memberIds = selectedMembers.value.map(m => m.userId)
|
||||
await imAdminApi.createGroup(appId, createGroupForm.value.name, createGroupForm.value.creatorId, memberIds)
|
||||
const memberIds = selectedMembers.value.map(item => item.userId)
|
||||
await imAdminApi.createGroup(appId, createGroupForm.value.name.trim(), createGroupForm.value.creatorId.trim(), memberIds)
|
||||
ElMessage.success('群组创建成功')
|
||||
showCreateGroup.value = false
|
||||
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(() => {
|
||||
loadStats()
|
||||
loadUsers()
|
||||
@ -329,10 +554,28 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card { display: flex; flex-direction: column; 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; }
|
||||
.stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
width: 100%;
|
||||
@ -343,6 +586,7 @@ onMounted(() => {
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -351,9 +595,20 @@ onMounted(() => {
|
||||
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; }
|
||||
|
||||
.search-result-item:hover {
|
||||
background: #f0f4ff;
|
||||
}
|
||||
|
||||
.result-nickname {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.result-userid {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.selected-members {
|
||||
display: flex;
|
||||
@ -362,5 +617,15 @@ onMounted(() => {
|
||||
min-height: 32px;
|
||||
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>
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户