feat: 消息中心接入部分接口,调整样式
This commit is contained in:
parent
3e1f1fdc67
commit
62143affd5
@ -47,14 +47,18 @@ export interface ChatMessage {
|
||||
// 群聊成员接口类型定义
|
||||
export interface ChatMember {
|
||||
id: string
|
||||
chatId: string
|
||||
userId: string
|
||||
userName: string
|
||||
userAvatar?: string
|
||||
role: 'admin' | 'member'
|
||||
joinTime: string
|
||||
phone: string
|
||||
sex: number
|
||||
avatar: string
|
||||
isTeacher: boolean
|
||||
email: string
|
||||
realname: string
|
||||
username: string
|
||||
// 可选字段
|
||||
chatId?: string
|
||||
role?: 'admin' | 'member'
|
||||
joinTime?: string
|
||||
isOnline?: boolean
|
||||
// 根据接口文档优化的字段
|
||||
status?: number // 成员状态
|
||||
lastActiveTime?: string // 最后活跃时间
|
||||
}
|
||||
@ -173,5 +177,29 @@ export const ChatApi = {
|
||||
*/
|
||||
getUnreadCount: (): Promise<ApiResponse<{ total: number; chats: { chatId: string; count: number }[] }>> => {
|
||||
return ApiRequest.get('/aiol/aiolChat/unread-count')
|
||||
},
|
||||
|
||||
/**
|
||||
* 开启全员禁言
|
||||
* POST /aiol/aiolChat/{chatId}/mute_all
|
||||
*/
|
||||
muteAll: (chatId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.post(`/aiol/aiolChat/${chatId}/mute_all`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭全员禁言
|
||||
* POST /aiol/aiolChat/{chatId}/unmute_all
|
||||
*/
|
||||
unmuteAll: (chatId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.post(`/aiol/aiolChat/${chatId}/unmute_all`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询会话详情
|
||||
* GET /aiol/aiolChat/{chatId}/detail
|
||||
*/
|
||||
getChatDetail: (chatId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.get(`/aiol/aiolChat/${chatId}/detail`)
|
||||
}
|
||||
}
|
||||
|
@ -284,8 +284,8 @@ const handleInput = (value: string) => {
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Ctrl/Cmd + Enter 发送消息
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
// Enter 键发送消息
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
|
@ -43,7 +43,6 @@
|
||||
{{ contact.name }}
|
||||
<span v-if="contact.type === 'group'" class="member-count">({{ contact.memberCount || 0 }}人)</span>
|
||||
</span>
|
||||
<span class="contact-time">{{ contact.lastMessageTime }}</span>
|
||||
</div>
|
||||
<div class="contact-preview">
|
||||
<span class="last-message">{{ contact.lastMessage }}</span>
|
||||
@ -170,6 +169,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 禁言状态提示 -->
|
||||
<div v-if="activeContact?.type === 'group' && isAllMuted" class="mute-status-tip">
|
||||
<div class="mute-tip-content">
|
||||
<svg class="mute-icon" viewBox="0 0 24 24" width="14" height="14">
|
||||
<path fill="currentColor"
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
</svg>
|
||||
<span>群聊已开启全员禁言</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -185,21 +195,40 @@
|
||||
<path fill="currentColor"
|
||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||||
</svg>
|
||||
<input type="text" placeholder="搜索群成员" class="search-input">
|
||||
<input type="text" placeholder="搜索群成员" class="search-input" v-model="memberSearchKeyword"
|
||||
@input="handleMemberSearch">
|
||||
<div v-if="isSearchingMembers" class="search-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群成员列表 -->
|
||||
<div class="members-section">
|
||||
<div class="members-grid">
|
||||
<div v-for="i in 20" :key="i" class="member-item">
|
||||
<!-- 有搜索结果时显示成员列表 -->
|
||||
<div v-if="displayedMembers.length > 0" class="members-grid">
|
||||
<div v-for="member in displayedMembers" :key="member.id" class="member-item">
|
||||
<div class="member-avatar">
|
||||
<img src="https://avatars.githubusercontent.com/u/38358644" alt="成员头像" />
|
||||
<img :src="member.avatar || '/images/profile/default-avatar.png'" :alt="member.realname" />
|
||||
</div>
|
||||
<div class="member-name">李小多</div>
|
||||
<div class="member-name">{{ member.realname }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-more">
|
||||
|
||||
<!-- 搜索无结果时的空状态 -->
|
||||
<div v-else-if="memberSearchKeyword.trim()" class="empty-search-state">
|
||||
<div class="empty-icon">
|
||||
<svg viewBox="0 0 24 24" width="48" height="48">
|
||||
<path fill="currentColor"
|
||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="empty-text">未找到相关成员</div>
|
||||
<div class="empty-hint">请尝试其他关键词</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看更多按钮 -->
|
||||
<div v-if="shouldShowViewMore" class="view-more" @click="showAllMembers = true">
|
||||
<span>查看更多</span>
|
||||
<svg class="chevron-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||
<path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" />
|
||||
@ -211,11 +240,11 @@
|
||||
<div class="info-section">
|
||||
<div class="info-item">
|
||||
<span class="info-label">班级名称</span>
|
||||
<span class="info-value">计算机一班</span>
|
||||
<span class="info-value">{{ groupInfo.name || '暂无' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">班级人数</span>
|
||||
<span class="info-value">149人</span>
|
||||
<span class="info-value">{{ groupInfo.memberCount }}人</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -231,8 +260,9 @@
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">全员禁言</span>
|
||||
<div class="switch-wrapper">
|
||||
<input type="checkbox" id="muteAll" v-model="groupSettings.muteAll" class="switch-input">
|
||||
<label for="muteAll" class="switch-label"></label>
|
||||
<input type="checkbox" id="muteAll" v-model="isAllMuted" :disabled="isMuteLoading"
|
||||
@change="handleMuteAllToggle" class="switch-input">
|
||||
<label for="muteAll" class="switch-label" :class="{ 'loading': isMuteLoading }"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
@ -328,6 +358,26 @@ interface Contact {
|
||||
unreadCount: number
|
||||
isOnline?: boolean
|
||||
memberCount?: number
|
||||
izAllMuted?: boolean | number // 全员禁言状态,支持布尔值和数字(1/0)
|
||||
}
|
||||
|
||||
// 群聊成员类型定义
|
||||
interface ChatMember {
|
||||
id: string
|
||||
phone: string
|
||||
sex: number
|
||||
avatar: string
|
||||
isTeacher: boolean
|
||||
email: string
|
||||
realname: string
|
||||
username: string
|
||||
// 可选字段
|
||||
chatId?: string
|
||||
role?: 'admin' | 'member'
|
||||
joinTime?: string
|
||||
isOnline?: boolean
|
||||
status?: number // 成员状态
|
||||
lastActiveTime?: string // 最后活跃时间
|
||||
}
|
||||
|
||||
// 消息类型定义(兼容API返回的数据)
|
||||
@ -382,6 +432,55 @@ const contacts = ref<Contact[]>([])
|
||||
// 当前会话的消息数据
|
||||
const messages = ref<Message[]>([])
|
||||
|
||||
// 群成员数据
|
||||
const groupMembers = ref<ChatMember[]>([])
|
||||
|
||||
// 群组信息数据
|
||||
const groupInfo = ref({
|
||||
name: '',
|
||||
memberCount: 0
|
||||
})
|
||||
|
||||
// 全员禁言状态
|
||||
const isAllMuted = ref(false)
|
||||
const isMuteLoading = ref(false)
|
||||
|
||||
// 群成员显示控制
|
||||
const showAllMembers = ref(false)
|
||||
const maxDisplayMembers = 20 // 5行 × 4列 = 20个成员
|
||||
|
||||
// 群成员搜索
|
||||
const memberSearchKeyword = ref('')
|
||||
const isSearchingMembers = ref(false)
|
||||
|
||||
// 计算属性:过滤后的群成员列表
|
||||
const filteredMembers = computed(() => {
|
||||
if (!memberSearchKeyword.value.trim()) {
|
||||
return groupMembers.value
|
||||
}
|
||||
|
||||
const keyword = memberSearchKeyword.value.toLowerCase().trim()
|
||||
return groupMembers.value.filter((member: any) =>
|
||||
member.realname.toLowerCase().includes(keyword) ||
|
||||
member.username.toLowerCase().includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
// 计算属性:显示的群成员列表
|
||||
const displayedMembers = computed(() => {
|
||||
const members = filteredMembers.value
|
||||
|
||||
if (showAllMembers.value || members.length <= maxDisplayMembers) {
|
||||
return members
|
||||
}
|
||||
return members.slice(0, maxDisplayMembers)
|
||||
})
|
||||
|
||||
// 计算属性:是否需要显示"查看更多"按钮
|
||||
const shouldShowViewMore = computed(() => {
|
||||
return filteredMembers.value.length > maxDisplayMembers && !showAllMembers.value
|
||||
})
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
loadContacts()
|
||||
@ -407,7 +506,8 @@ const loadContacts = async () => {
|
||||
lastMessageTime: formatTime(chat.lastMessageTime || chat.updateTime),
|
||||
unreadCount: chat.unreadCount || 0,
|
||||
isOnline: chat.isOnline,
|
||||
memberCount: chat.memberCount || (contactType === 'group' ? 0 : undefined)
|
||||
memberCount: chat.memberCount || (contactType === 'group' ? 0 : undefined),
|
||||
izAllMuted: chat.izAllMuted === 1
|
||||
}
|
||||
})
|
||||
|
||||
@ -446,6 +546,112 @@ const loadGroupMemberCount = async (chatId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载群成员列表
|
||||
const loadGroupMembers = async (chatId: string) => {
|
||||
try {
|
||||
const response = await ChatApi.getChatMembers(chatId)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
groupMembers.value = response.data.result || []
|
||||
console.log('群成员加载成功:', groupMembers.value.length, '个成员')
|
||||
|
||||
// 从联系人数据中获取群组名称和禁言状态
|
||||
const contact = contacts.value.find((c: Contact) => c.id === chatId)
|
||||
const groupName = contact ? contact.name : '暂无'
|
||||
|
||||
// 更新群组信息
|
||||
groupInfo.value = {
|
||||
name: groupName,
|
||||
memberCount: groupMembers.value.length
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('API响应失败:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取群成员失败:', error)
|
||||
message.error('获取群成员失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理群成员搜索
|
||||
const handleMemberSearch = () => {
|
||||
isSearchingMembers.value = true
|
||||
|
||||
// 模拟搜索延迟
|
||||
setTimeout(() => {
|
||||
isSearchingMembers.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 获取群聊详情(包括禁言状态)
|
||||
const loadChatDetail = async (chatId: string) => {
|
||||
try {
|
||||
const response = await ChatApi.getChatDetail(chatId)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const chatDetail = response.data.result
|
||||
|
||||
// 更新禁言状态
|
||||
if (chatDetail.izAllMuted !== undefined) {
|
||||
isAllMuted.value = chatDetail.izAllMuted === 1
|
||||
} else {
|
||||
// 备用方案:从联系人数据中获取禁言状态
|
||||
const contact = contacts.value.find((c: Contact) => c.id === chatId)
|
||||
if (contact && contact.izAllMuted !== undefined) {
|
||||
isAllMuted.value = contact.izAllMuted === 1 || contact.izAllMuted === true
|
||||
}
|
||||
}
|
||||
|
||||
return chatDetail
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取群聊详情失败:', error)
|
||||
|
||||
// 如果API调用失败,尝试从联系人数据中获取状态
|
||||
const contact = contacts.value.find((c: Contact) => c.id === chatId)
|
||||
if (contact && contact.izAllMuted !== undefined) {
|
||||
isAllMuted.value = contact.izAllMuted === 1 || contact.izAllMuted === true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理全员禁言切换
|
||||
const handleMuteAllToggle = async () => {
|
||||
if (!activeContactId.value) return
|
||||
|
||||
isMuteLoading.value = true
|
||||
|
||||
// 保存当前状态,用于错误回滚
|
||||
const previousState = isAllMuted.value
|
||||
|
||||
try {
|
||||
if (isAllMuted.value) {
|
||||
// 开启全员禁言
|
||||
await ChatApi.muteAll(activeContactId.value)
|
||||
message.success('已开启全员禁言')
|
||||
} else {
|
||||
// 关闭全员禁言
|
||||
await ChatApi.unmuteAll(activeContactId.value)
|
||||
message.success('已关闭全员禁言')
|
||||
}
|
||||
|
||||
// 更新联系人列表中的状态
|
||||
const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value)
|
||||
if (contact) {
|
||||
contact.izAllMuted = isAllMuted.value ? 1 : 0
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('切换全员禁言状态失败:', error)
|
||||
message.error('操作失败,请重试')
|
||||
// 回滚状态
|
||||
isAllMuted.value = previousState
|
||||
} finally {
|
||||
isMuteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载指定会话的消息
|
||||
const loadMessages = async (chatId: string) => {
|
||||
messagesLoading.value = true
|
||||
@ -538,6 +744,16 @@ const currentMessages = computed(() => {
|
||||
const selectContact = async (contactId: string) => {
|
||||
activeContactId.value = contactId
|
||||
|
||||
// 重置群成员展开状态、群组信息和搜索状态
|
||||
showAllMembers.value = false
|
||||
memberSearchKeyword.value = ''
|
||||
isSearchingMembers.value = false
|
||||
isMuteLoading.value = false
|
||||
groupInfo.value = {
|
||||
name: '',
|
||||
memberCount: 0
|
||||
}
|
||||
|
||||
// 清除未读数量
|
||||
const contact = contacts.value.find((c: Contact) => c.id === contactId)
|
||||
if (contact) {
|
||||
@ -547,17 +763,23 @@ const selectContact = async (contactId: string) => {
|
||||
if (contact.type === 'group' && (!contact.memberCount || contact.memberCount === 0)) {
|
||||
loadGroupMemberCount(contactId)
|
||||
}
|
||||
|
||||
// 如果是群聊,加载群成员列表和群聊详情
|
||||
if (contact.type === 'group') {
|
||||
loadGroupMembers(contactId)
|
||||
loadChatDetail(contactId) // 获取群聊详情,包括禁言状态
|
||||
}
|
||||
}
|
||||
|
||||
// 加载该会话的消息
|
||||
await loadMessages(contactId)
|
||||
|
||||
// 标记消息为已读
|
||||
try {
|
||||
await ChatApi.markAsRead(contactId)
|
||||
} catch (error) {
|
||||
console.warn('标记消息已读失败:', error)
|
||||
}
|
||||
// 暂时移除标记消息已读的API调用,避免错误
|
||||
// try {
|
||||
// await ChatApi.markAsRead(contactId)
|
||||
// } catch (error) {
|
||||
// console.warn('标记消息已读失败:', error)
|
||||
// }
|
||||
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
@ -1011,7 +1233,6 @@ onMounted(() => {
|
||||
|
||||
.contact-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
color: #3F3F44;
|
||||
@ -1033,11 +1254,6 @@ onMounted(() => {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.contact-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-preview {
|
||||
display: flex;
|
||||
@ -1321,6 +1537,33 @@ onMounted(() => {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 禁言状态提示样式 */
|
||||
.mute-status-tip {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 12px 0 8px 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.mute-tip-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #e0e6ec;
|
||||
color: #6c757d;
|
||||
padding: 2px 8px;
|
||||
border-radius: 30px;
|
||||
font-size: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
max-width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mute-icon {
|
||||
color: #6c757d;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-bubble {
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
@ -1473,6 +1716,62 @@ onMounted(() => {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.search-loading {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-search-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
color: #d9d9d9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.members-section {
|
||||
margin: 0 20px;
|
||||
border-bottom: 1.5px solid #E6E6E6;
|
||||
@ -1526,20 +1825,28 @@ onMounted(() => {
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 15px;
|
||||
transition: color 0.2s ease;
|
||||
margin-top: 8px;
|
||||
padding: 6px 12px;
|
||||
background: #f8f8f8;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.view-more:hover {
|
||||
color: #1890ff;
|
||||
background: #f0f8ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
color: #666666;
|
||||
transition: color 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.view-more:hover .chevron-icon {
|
||||
color: #1890ff;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
/* 私聊详情样式 */
|
||||
@ -1670,6 +1977,24 @@ onMounted(() => {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.switch-label.loading {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.switch-label.loading:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: -6px 0 0 -6px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #0288D1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.actions-section {
|
||||
margin: 0 20px;
|
||||
display: flex;
|
||||
|
Loading…
x
Reference in New Issue
Block a user