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
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,20 +13,20 @@
</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">
<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 }">
{{ { UNKNOWN: '未知', MALE: '男', FEMALE: '女' }[row.gender] }}
{{ genderLabel[row.gender] ?? '未知' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
@ -38,20 +37,24 @@
</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">
<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>
</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>