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 {
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`)
}
}

View File

@ -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()
}

View File

@ -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;