feat: 消息中心接入部分接口,调整样式

This commit is contained in:
QDKF 2025-09-22 14:58:12 +08:00
parent 3e1f1fdc67
commit 62143affd5
3 changed files with 388 additions and 35 deletions

View File

@ -47,14 +47,18 @@ export interface ChatMessage {
// 群聊成员接口类型定义 // 群聊成员接口类型定义
export interface ChatMember { export interface ChatMember {
id: string id: string
chatId: string phone: string
userId: string sex: number
userName: string avatar: string
userAvatar?: string isTeacher: boolean
role: 'admin' | 'member' email: string
joinTime: string realname: string
username: string
// 可选字段
chatId?: string
role?: 'admin' | 'member'
joinTime?: string
isOnline?: boolean isOnline?: boolean
// 根据接口文档优化的字段
status?: number // 成员状态 status?: number // 成员状态
lastActiveTime?: string // 最后活跃时间 lastActiveTime?: string // 最后活跃时间
} }
@ -173,5 +177,29 @@ export const ChatApi = {
*/ */
getUnreadCount: (): Promise<ApiResponse<{ total: number; chats: { chatId: string; count: number }[] }>> => { getUnreadCount: (): Promise<ApiResponse<{ total: number; chats: { chatId: string; count: number }[] }>> => {
return ApiRequest.get('/aiol/aiolChat/unread-count') 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`)
} }
} }

View File

@ -284,8 +284,8 @@ const handleInput = (value: string) => {
} }
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
// Ctrl/Cmd + Enter // Enter
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault() event.preventDefault()
handleSend() handleSend()
} }

View File

@ -43,7 +43,6 @@
{{ contact.name }} {{ contact.name }}
<span v-if="contact.type === 'group'" class="member-count">({{ contact.memberCount || 0 }})</span> <span v-if="contact.type === 'group'" class="member-count">({{ contact.memberCount || 0 }})</span>
</span> </span>
<span class="contact-time">{{ contact.lastMessageTime }}</span>
</div> </div>
<div class="contact-preview"> <div class="contact-preview">
<span class="last-message">{{ contact.lastMessage }}</span> <span class="last-message">{{ contact.lastMessage }}</span>
@ -170,6 +169,17 @@
</div> </div>
</div> </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>
</div> </div>
@ -185,21 +195,40 @@
<path fill="currentColor" <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" /> 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> </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> </div>
<!-- 群成员列表 --> <!-- 群成员列表 -->
<div class="members-section"> <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"> <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>
<div class="member-name">李小多</div> <div class="member-name">{{ member.realname }}</div>
</div> </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> <span>查看更多</span>
<svg class="chevron-icon" viewBox="0 0 24 24" width="16" height="16"> <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" /> <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-section">
<div class="info-item"> <div class="info-item">
<span class="info-label">班级名称</span> <span class="info-label">班级名称</span>
<span class="info-value">计算机一班</span> <span class="info-value">{{ groupInfo.name || '暂无' }}</span>
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">班级人数</span> <span class="info-label">班级人数</span>
<span class="info-value">149</span> <span class="info-value">{{ groupInfo.memberCount }}</span>
</div> </div>
</div> </div>
@ -231,8 +260,9 @@
<div class="setting-item"> <div class="setting-item">
<span class="setting-label">全员禁言</span> <span class="setting-label">全员禁言</span>
<div class="switch-wrapper"> <div class="switch-wrapper">
<input type="checkbox" id="muteAll" v-model="groupSettings.muteAll" class="switch-input"> <input type="checkbox" id="muteAll" v-model="isAllMuted" :disabled="isMuteLoading"
<label for="muteAll" class="switch-label"></label> @change="handleMuteAllToggle" class="switch-input">
<label for="muteAll" class="switch-label" :class="{ 'loading': isMuteLoading }"></label>
</div> </div>
</div> </div>
<div class="setting-item"> <div class="setting-item">
@ -328,6 +358,26 @@ interface Contact {
unreadCount: number unreadCount: number
isOnline?: boolean isOnline?: boolean
memberCount?: number 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 // API
@ -382,6 +432,55 @@ const contacts = ref<Contact[]>([])
// //
const messages = ref<Message[]>([]) 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(() => { onMounted(() => {
loadContacts() loadContacts()
@ -407,7 +506,8 @@ const loadContacts = async () => {
lastMessageTime: formatTime(chat.lastMessageTime || chat.updateTime), lastMessageTime: formatTime(chat.lastMessageTime || chat.updateTime),
unreadCount: chat.unreadCount || 0, unreadCount: chat.unreadCount || 0,
isOnline: chat.isOnline, 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) => { const loadMessages = async (chatId: string) => {
messagesLoading.value = true messagesLoading.value = true
@ -538,6 +744,16 @@ const currentMessages = computed(() => {
const selectContact = async (contactId: string) => { const selectContact = async (contactId: string) => {
activeContactId.value = contactId 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) const contact = contacts.value.find((c: Contact) => c.id === contactId)
if (contact) { if (contact) {
@ -547,17 +763,23 @@ const selectContact = async (contactId: string) => {
if (contact.type === 'group' && (!contact.memberCount || contact.memberCount === 0)) { if (contact.type === 'group' && (!contact.memberCount || contact.memberCount === 0)) {
loadGroupMemberCount(contactId) loadGroupMemberCount(contactId)
} }
//
if (contact.type === 'group') {
loadGroupMembers(contactId)
loadChatDetail(contactId) //
}
} }
// //
await loadMessages(contactId) await loadMessages(contactId)
// // API
try { // try {
await ChatApi.markAsRead(contactId) // await ChatApi.markAsRead(contactId)
} catch (error) { // } catch (error) {
console.warn('标记消息已读失败:', error) // console.warn(':', error)
} // }
nextTick(() => { nextTick(() => {
scrollToBottom() scrollToBottom()
@ -1011,7 +1233,6 @@ onMounted(() => {
.contact-header { .contact-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
color: #3F3F44; color: #3F3F44;
@ -1033,11 +1254,6 @@ onMounted(() => {
margin-left: 4px; margin-left: 4px;
} }
.contact-time {
font-size: 12px;
color: #999;
flex-shrink: 0;
}
.contact-preview { .contact-preview {
display: flex; display: flex;
@ -1321,6 +1537,33 @@ onMounted(() => {
word-break: break-word; 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 { .image-bubble {
padding: 4px; padding: 4px;
background: transparent; background: transparent;
@ -1473,6 +1716,62 @@ onMounted(() => {
border-color: #1890ff; 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 { .members-section {
margin: 0 20px; margin: 0 20px;
border-bottom: 1.5px solid #E6E6E6; border-bottom: 1.5px solid #E6E6E6;
@ -1526,20 +1825,28 @@ onMounted(() => {
font-size: 10px; font-size: 10px;
cursor: pointer; cursor: pointer;
margin-bottom: 15px; 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 { .view-more:hover {
color: #1890ff; color: #1890ff;
background: #f0f8ff;
border-color: #1890ff;
} }
.chevron-icon { .chevron-icon {
color: #666666; color: #666666;
transition: color 0.2s ease; transition: all 0.2s ease;
} }
.view-more:hover .chevron-icon { .view-more:hover .chevron-icon {
color: #1890ff; color: #1890ff;
transform: translateY(1px);
} }
/* 私聊详情样式 */ /* 私聊详情样式 */
@ -1670,6 +1977,24 @@ onMounted(() => {
transform: translateX(20px); 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 { .actions-section {
margin: 0 20px; margin: 0 20px;
display: flex; display: flex;