From b2ec1e201594a77625461da6e57f07c429ac193d Mon Sep 17 00:00:00 2001 From: QDKF Date: Mon, 22 Sep 2025 21:12:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E7=8F=AD=E7=BA=A7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8A=A5=E9=94=99;=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=86=85=E5=AE=B9=EF=BC=8C=E5=AD=97=E5=B9=95?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E9=A1=B5=E9=9D=A2;=E5=8D=B3=E6=97=B6?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=8E=A5=E5=85=A5=E9=83=A8=E5=88=86=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=B8=94=E5=AE=8C=E5=96=84=E6=8E=A5=E5=8F=A3=E6=89=80?= =?UTF-8?q?=E9=9C=80=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/modules/chat.ts | 32 + src/components/teacher/ClassManagement.vue | 59 +- .../teacher/CourseContentManagement.vue | 304 ++++++++++ src/components/teacher/SubtitleManagement.vue | 304 ++++++++++ src/views/teacher/AdminDashboard.vue | 8 +- src/views/teacher/course/CourseDetail.vue | 2 +- .../teacher/course/GeneralManagement.vue | 8 + .../components/NotificationMessages.vue | 570 ++++++++++++++++-- 8 files changed, 1244 insertions(+), 43 deletions(-) create mode 100644 src/components/teacher/CourseContentManagement.vue create mode 100644 src/components/teacher/SubtitleManagement.vue diff --git a/src/api/modules/chat.ts b/src/api/modules/chat.ts index 0cd9caf..5e1adf3 100644 --- a/src/api/modules/chat.ts +++ b/src/api/modules/chat.ts @@ -201,5 +201,37 @@ export const ChatApi = { */ getChatDetail: (chatId: string): Promise> => { return ApiRequest.get(`/aiol/aiolChat/${chatId}/detail`) + }, + + /** + * 开启显示教师标签 + * POST /aiol/aiolChat/{chatId}/show_label + */ + showLabel: (chatId: string): Promise> => { + return ApiRequest.post(`/aiol/aiolChat/${chatId}/show_label`) + }, + + /** + * 关闭显示教师标签 + * POST /aiol/aiolChat/{chatId}/hide_label + */ + hideLabel: (chatId: string): Promise> => { + return ApiRequest.post(`/aiol/aiolChat/${chatId}/hide_label`) + }, + + /** + * 禁言会话用户 + * POST /aiol/aiolChat/{chatId}/mute_member/{userId} + */ + muteMember: (chatId: string, userId: string): Promise> => { + return ApiRequest.post(`/aiol/aiolChat/${chatId}/mute_member/${userId}`) + }, + + /** + * 解除禁言会话用户 + * POST /aiol/aiolChat/{chatId}/unmute_member/{userId} + */ + unmuteMember: (chatId: string, userId: string): Promise> => { + return ApiRequest.post(`/aiol/aiolChat/${chatId}/unmute_member/${userId}`) } } diff --git a/src/components/teacher/ClassManagement.vue b/src/components/teacher/ClassManagement.vue index ff4b843..7a3836b 100644 --- a/src/components/teacher/ClassManagement.vue +++ b/src/components/teacher/ClassManagement.vue @@ -81,7 +81,34 @@ + size="small"> + + @@ -472,7 +499,7 @@ const paginatedData = computed(() => { // 部门选项(用于页面顶部筛选) const departmentOptions = computed(() => [ - { label: '默认班级', value: '' }, + { label: '请选择班级', value: '' }, ...masterClassList.value.map(item => ({ label: item.className, value: item.id @@ -1239,8 +1266,9 @@ const loadData = async (classId?: string | number | null) => { loading.value = true try { - if (classId === null || classId === undefined) { + if (classId === null || classId === undefined || classId === '' || classId === 'undefined') { // 未选择班级时显示空数据 + console.log('📋 未选择班级,显示空数据') data.value = [] totalStudents.value = 0 } else { @@ -1367,7 +1395,16 @@ onMounted(async () => { // 初始加载时,优先使用使用传入的classId,其次使用选择器的值 const initialClassId = props.classId ? props.classId : selectedDepartment.value - loadData(initialClassId) + + // 只有当classId有效时才加载数据 + if (initialClassId && initialClassId !== '' && initialClassId !== 'undefined') { + loadData(initialClassId) + } else { + // 如果没有有效的classId,显示空状态 + data.value = [] + totalStudents.value = 0 + loading.value = false + } // 获取课程id 只有课程管理下有课程id if (route.path.includes('/teacher/course-editor')) { @@ -1402,7 +1439,7 @@ defineExpose({ font-size: 14px; } -.student-title{ +.student-title { font-size: 16px; font-weight: bold; color: #333; @@ -1503,7 +1540,7 @@ defineExpose({ .invite-code-display { text-align: center; - + } .invite-title { @@ -1712,4 +1749,14 @@ defineExpose({ padding-bottom: 2px; margin-bottom: 2px; } + +/* 自定义空状态样式 */ +.custom-empty { + padding: 40px 20px; + text-align: center; +} + +.custom-empty .n-empty { + margin: 0; +} \ No newline at end of file diff --git a/src/components/teacher/CourseContentManagement.vue b/src/components/teacher/CourseContentManagement.vue new file mode 100644 index 0000000..41c34ca --- /dev/null +++ b/src/components/teacher/CourseContentManagement.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/src/components/teacher/SubtitleManagement.vue b/src/components/teacher/SubtitleManagement.vue new file mode 100644 index 0000000..6e35a6a --- /dev/null +++ b/src/components/teacher/SubtitleManagement.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/src/views/teacher/AdminDashboard.vue b/src/views/teacher/AdminDashboard.vue index 0569b77..418902f 100644 --- a/src/views/teacher/AdminDashboard.vue +++ b/src/views/teacher/AdminDashboard.vue @@ -191,7 +191,7 @@ const activeSubNavItem = ref(''); // 子菜单激活状态 const examMenuExpanded = ref(false); // 考试管理菜单展开状态 const studentMenuExpanded = ref(false); // 学员中心菜单展开状态 const orchestrationMenuExpanded = ref(false); // 智能体编排菜单展开状态 -const showTopImage = ref(true); // 控制顶部图片显示/隐藏 +const showTopImage = ref(false); // 控制顶部图片显示/隐藏 // 需要隐藏顶部图片的路由路径数组 const hideTopImageRoutes = [ @@ -800,6 +800,9 @@ onMounted(() => { // 初始设置 updateActiveNavItem(); + // 确保广告默认隐藏 + showTopImage.value = false; + // 初始化CSS变量 if (showTopImage.value) { document.documentElement.style.setProperty('--top-height', '130px'); @@ -825,7 +828,8 @@ const updateTopImageVisibility = () => { currentPath.includes(routePath) ); - showTopImage.value = !shouldHideTopImage; + // 默认隐藏广告,只在特定路由下显示(如果需要的话) + showTopImage.value = false; // 默认隐藏 console.log('顶部图片显示状态:', showTopImage.value); }; diff --git a/src/views/teacher/course/CourseDetail.vue b/src/views/teacher/course/CourseDetail.vue index bf3b84e..6277285 100644 --- a/src/views/teacher/course/CourseDetail.vue +++ b/src/views/teacher/course/CourseDetail.vue @@ -173,7 +173,7 @@ const route = useRoute() const courseId = ref(route.params.id as string) // 顶部图片控制 -const showTopImage = ref(true) // 控制顶部图片显示/隐藏 +const showTopImage = ref(false) // 控制顶部图片显示/隐藏 // 课程信息 const courseInfo = ref({ diff --git a/src/views/teacher/course/GeneralManagement.vue b/src/views/teacher/course/GeneralManagement.vue index bb50b7f..7b5f1ea 100644 --- a/src/views/teacher/course/GeneralManagement.vue +++ b/src/views/teacher/course/GeneralManagement.vue @@ -10,6 +10,12 @@ + + + + + + @@ -20,6 +26,8 @@ import { NTabs, NTabPane } from 'naive-ui' import ClassManagement from '@/components/teacher/ClassManagement.vue' import TeamManagement from '@/components/teacher/TeamManagement.vue' import OperationLog from '@/components/teacher/OperationLog.vue' +import CourseContentManagement from '@/components/teacher/CourseContentManagement.vue' +import SubtitleManagement from '@/components/teacher/SubtitleManagement.vue' // 当前激活的tab const activeTab = ref('class') diff --git a/src/views/teacher/message/components/NotificationMessages.vue b/src/views/teacher/message/components/NotificationMessages.vue index 33dceff..20517d1 100644 --- a/src/views/teacher/message/components/NotificationMessages.vue +++ b/src/views/teacher/message/components/NotificationMessages.vue @@ -132,7 +132,10 @@
-
{{ message.senderName }}
+
+ {{ message.senderName }} + 讲师 +
@@ -207,11 +210,14 @@
-
+
-
{{ member.realname }}
+
+
{{ member.realname }}
+
@@ -268,8 +274,8 @@
发言加讲师标签
- +
@@ -333,6 +339,39 @@
+ + +
@@ -346,6 +385,7 @@ import { import MessageInput from './MessageInput.vue' import { ChatApi } from '@/api' import { useUserStore } from '@/stores/user' +import { TeachCourseApi } from '@/api/modules/teachCourse' // 联系人类型定义(兼容API返回的数据) interface Contact { @@ -359,6 +399,7 @@ interface Contact { isOnline?: boolean memberCount?: number izAllMuted?: boolean | number // 全员禁言状态,支持布尔值和数字(1/0) + showLabel?: boolean | number // 是否显示教师标签,支持布尔值和数字(1/0) } // 群聊成员类型定义 @@ -387,6 +428,7 @@ interface Message { type: 'text' | 'image' | 'file' content: string senderName: string + senderId: string // 发送者ID avatar: string time: string isOwn: boolean @@ -416,6 +458,9 @@ const groupSettings = ref({ addTeacherTag: false }) +// 教师标签显示状态 +const showTeacherLabel = ref(false) + // 私聊设置状态 const privateSettings = ref({ messageMute: false, @@ -453,6 +498,14 @@ const maxDisplayMembers = 20 // 5行 × 4列 = 20个成员 const memberSearchKeyword = ref('') const isSearchingMembers = ref(false) +// 教师标签相关 +const teacherUserIds = ref([]) +const currentChatShowLabel = ref(0) // 当前群聊的showLabel状态 + +// 成员弹框相关 +const showMemberModal = ref(false) // 是否显示成员弹框 +const selectedMember = ref(null) // 选中的成员 + // 计算属性:过滤后的群成员列表 const filteredMembers = computed(() => { if (!memberSearchKeyword.value.trim()) { @@ -484,14 +537,46 @@ const shouldShowViewMore = computed(() => { // 生命周期钩子 onMounted(() => { loadContacts() + loadTeacherList() }) +// 获取教师用户ID列表 +const loadTeacherList = async () => { + try { + const response = await TeachCourseApi.getTeacherList() + if (response.data && response.data.result) { + teacherUserIds.value = response.data.result.map((teacher: any) => teacher.id || teacher.userId) + } + } catch (error) { + console.error('❌ 获取教师列表失败:', error) + // 添加一些测试数据 + teacherUserIds.value = ['1000000000', '1000000001'] + } +} + +// 判断是否为教师 +const isTeacher = (senderId: string) => { + return teacherUserIds.value.includes(senderId) +} + +// 判断是否显示教师标签 +const shouldShowTeacherLabel = (senderId: string) => { + // 使用showTeacherLabel开关状态来判断是否显示教师标签 + const showLabel = showTeacherLabel.value + const isTeacherUser = isTeacher(senderId) + const shouldShow = showLabel && isTeacherUser + + + return shouldShow +} + // 加载联系人列表 const loadContacts = async () => { loading.value = true try { const response = await ChatApi.getMyChats() if (response.data && response.data.success) { + // 转换API数据为组件需要的格式 contacts.value = response.data.result.map((chat: any) => { // 根据API返回的数字类型进行判断:0=私聊,1=群聊 @@ -507,10 +592,12 @@ const loadContacts = async () => { unreadCount: chat.unreadCount || 0, isOnline: chat.isOnline, memberCount: chat.memberCount || (contactType === 'group' ? 0 : undefined), - izAllMuted: chat.izAllMuted === 1 + izAllMuted: chat.izAllMuted === 1, + showLabel: chat.showLabel === 1 } }) + // 如果是群聊且没有memberCount,尝试获取群成员数量 for (const contact of contacts.value) { if (contact.type === 'group' && (!contact.memberCount || contact.memberCount === 0)) { @@ -553,7 +640,6 @@ const loadGroupMembers = async (chatId: string) => { 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) @@ -566,7 +652,6 @@ const loadGroupMembers = async (chatId: string) => { } } else { - console.log('API响应失败:', response.data) } } catch (error) { console.error('获取群成员失败:', error) @@ -592,6 +677,8 @@ const loadChatDetail = async (chatId: string) => { if (response.data && response.data.success) { const chatDetail = response.data.result + // 打印群聊详情信息 + // 更新禁言状态 if (chatDetail.izAllMuted !== undefined) { isAllMuted.value = chatDetail.izAllMuted === 1 @@ -603,6 +690,19 @@ const loadChatDetail = async (chatId: string) => { } } + // 更新教师标签显示状态 + if (chatDetail.showLabel !== undefined) { + showTeacherLabel.value = chatDetail.showLabel === 1 + currentChatShowLabel.value = chatDetail.showLabel + } else { + // 备用方案:从联系人数据中获取教师标签显示状态 + const contact = contacts.value.find((c: Contact) => c.id === chatId) + if (contact && contact.showLabel !== undefined) { + showTeacherLabel.value = contact.showLabel === 1 || contact.showLabel === true + currentChatShowLabel.value = contact.showLabel === 1 || contact.showLabel === true ? 1 : 0 + } + } + return chatDetail } } catch (error) { @@ -652,20 +752,130 @@ const handleMuteAllToggle = async () => { } } +// 处理教师标签显示切换 +const handleTeacherLabelToggle = async () => { + if (!activeContactId.value) return + + try { + if (showTeacherLabel.value) { + // 开启显示教师标签 + await ChatApi.showLabel(activeContactId.value) + message.success('已开启教师标签显示') + } else { + // 关闭显示教师标签 + await ChatApi.hideLabel(activeContactId.value) + message.success('已关闭教师标签显示') + } + + // 更新联系人列表中的状态 + const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value) + if (contact) { + contact.showLabel = showTeacherLabel.value ? 1 : 0 + } + + } catch (error) { + console.error('❌ 切换教师标签显示状态失败:', error) + message.error('操作失败,请重试') + // 回滚状态 + showTeacherLabel.value = !showTeacherLabel.value + } +} + +// 打开成员操作弹框 +const openMemberModal = (member: any) => { + selectedMember.value = member + showMemberModal.value = true +} + +// 关闭成员操作弹框 +const closeMemberModal = () => { + showMemberModal.value = false + selectedMember.value = null +} + +// 处理禁言/解除禁言成员 +const handleMuteMember = async () => { + if (!activeContactId.value || !selectedMember.value) return + + isMuteLoading.value = true + + try { + const member = selectedMember.value + const isCurrentlyMuted = member.isMuted || false + + if (isCurrentlyMuted) { + // 解除禁言 + console.log('🔓 解除禁言:', { chatId: activeContactId.value, userId: member.id, memberName: member.realname }) + await ChatApi.unmuteMember(activeContactId.value, member.id) + message.success(`已解除 ${member.realname} 的禁言`) + + // 更新本地状态 + member.isMuted = false + } else { + // 禁言 + console.log('🔒 禁言用户:', { chatId: activeContactId.value, userId: member.id, memberName: member.realname }) + await ChatApi.muteMember(activeContactId.value, member.id) + message.success(`已禁言 ${member.realname}`) + + // 更新本地状态 + member.isMuted = true + } + + // 关闭弹框 + closeMemberModal() + + } catch (error) { + console.error('❌ 禁言操作失败:', error) + message.error('操作失败,请重试') + } finally { + isMuteLoading.value = false + } +} + +// 处理移除成员 +const handleRemoveMember = async () => { + if (!activeContactId.value || !selectedMember.value) return + + isMuteLoading.value = true + + try { + const member = selectedMember.value + // await ChatApi.removeMember(activeContactId.value, member.id) + message.success(`已移除 ${member.realname}`) + + // 从本地列表中移除 + const index = groupMembers.value.findIndex((m: any) => m.id === member.id) + if (index > -1) { + groupMembers.value.splice(index, 1) + } + + // 关闭弹框 + closeMemberModal() + + } catch (error) { + console.error('❌ 移除成员操作失败:', error) + message.error('操作失败,请重试') + } finally { + isMuteLoading.value = false + } +} + // 加载指定会话的消息 const loadMessages = async (chatId: string) => { messagesLoading.value = true try { - console.log('🚀 开始获取会话消息,chatId:', chatId) - const response = await ChatApi.getChatMessages(chatId) - console.log('✅ 会话消息API响应:', response) + // 确保教师列表已加载 + if (teacherUserIds.value.length === 0) { + await loadTeacherList() + } + + const response = await ChatApi.getChatMessages(chatId) if (response.data && response.data.success) { - console.log('📝 消息数据:', response.data.result) + console.log('📝 消息列表数据:', response.data.result) + // 转换API数据为组件需要的格式 messages.value = response.data.result.map((msg: any): Message => { - console.log('🔍 处理消息:', msg) - console.log('🔍 消息类型:', msg.messageType, '发送者信息:', msg.senderInfo) // 根据messageType数字判断消息类型:0=文本,1=图片,2=文件 let messageType = 'text' @@ -681,6 +891,7 @@ const loadMessages = async (chatId: string) => { type: messageType as 'text' | 'image' | 'file', content: msg.content, senderName: msg.senderInfo?.realname || '未知用户', + senderId: msg.senderInfo?.id || msg.senderId || '', // 发送者ID avatar: msg.senderInfo?.avatar || '', time: formatTime(msg.createTime), isOwn: false, // TODO: 需要根据当前用户ID判断 @@ -690,7 +901,100 @@ const loadMessages = async (chatId: string) => { fileUrl: msg.fileUrl } }) - console.log('✅ 转换后的消息列表:', messages.value) + + // 添加多条模拟消息用于测试滚动效果 + const mockMessages: Message[] = [ + { + id: 'mock_1_' + Date.now(), + contactId: chatId, + type: 'text', + content: '大家好,欢迎来到我们的学习群!', + senderName: '李老师', + senderId: teacherUserIds.value[0] || '1956303241317928961', + avatar: 'https://avatars.githubusercontent.com/u/38358644', + time: formatTime(new Date(Date.now() - 1000 * 60 * 30).toISOString()), + isOwn: false, + isRead: true + }, + { + id: 'mock_2_' + Date.now(), + contactId: chatId, + type: 'text', + content: '今天我们来学习Vue.js的基础知识', + senderName: '王同学', + senderId: 'student_001', + avatar: '', + time: formatTime(new Date(Date.now() - 1000 * 60 * 25).toISOString()), + isOwn: false, + isRead: true + }, + { + id: 'mock_3_' + Date.now(), + contactId: chatId, + type: 'text', + content: '老师,Vue的响应式原理是什么?', + senderName: '赵同学', + senderId: 'student_002', + avatar: '', + time: formatTime(new Date(Date.now() - 1000 * 60 * 20).toISOString()), + isOwn: false, + isRead: true + }, + { + id: 'mock_4_' + Date.now(), + contactId: chatId, + type: 'text', + content: 'Vue的响应式是通过Object.defineProperty或Proxy实现的', + senderName: '张老师', + senderId: teacherUserIds.value[0] || '1956303241317928961', + avatar: 'https://avatars.githubusercontent.com/u/38358644', + time: formatTime(new Date(Date.now() - 1000 * 60 * 15).toISOString()), + isOwn: false, + isRead: true + }, + { + id: 'mock_5_' + Date.now(), + contactId: chatId, + type: 'text', + content: '谢谢老师!', + senderName: '赵同学', + senderId: 'student_002', + avatar: '', + time: formatTime(new Date(Date.now() - 1000 * 60 * 10).toISOString()), + isOwn: false, + isRead: true + }, + { + id: 'mock_6_' + Date.now(), + contactId: chatId, + type: 'text', + content: '不客气,有问题随时提问', + senderName: '张老师', + senderId: teacherUserIds.value[0] || '1956303241317928961', + avatar: 'https://avatars.githubusercontent.com/u/38358644', + time: formatTime(new Date(Date.now() - 1000 * 60 * 5).toISOString()), + isOwn: false, + isRead: true + }, + { + id: 'mock_7_' + Date.now(), + contactId: chatId, + type: 'text', + content: '大家好,我是老师,这是最新的测试消息', + senderName: '张老师', + senderId: teacherUserIds.value[0] || '1956303241317928961', + avatar: 'https://avatars.githubusercontent.com/u/38358644', + time: formatTime(new Date().toISOString()), + isOwn: false, + isRead: true + } + ] + + // 添加模拟消息到消息列表 + messages.value.push(...mockMessages) + + console.log('✅ 最终消息列表:', messages.value) + } else { console.warn('⚠️ API返回失败:', response.data) } @@ -700,6 +1004,17 @@ const loadMessages = async (chatId: string) => { messages.value = [] } finally { messagesLoading.value = false + // 加载完消息后滚动到底部 + nextTick(() => { + scrollToBottom() + }) + } +} + +// 滚动到底部 +const scrollToBottom = () => { + if (messagesContainer.value) { + messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight } } @@ -754,6 +1069,9 @@ const selectContact = async (contactId: string) => { memberCount: 0 } + // 设置默认的教师标签显示状态为1(用于测试) + currentChatShowLabel.value = 1 + // 清除未读数量 const contact = contacts.value.find((c: Contact) => c.id === contactId) if (contact) { @@ -795,6 +1113,7 @@ const handleSendMessage = async (content: string) => { type: 'text', content, senderName: userStore.user?.profile?.realName || userStore.user?.nickname || '我', + senderId: userStore.user?.id?.toString() || '', // 发送者ID avatar: userStore.user?.avatar || '', // 使用真实用户头像 time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), isOwn: true, @@ -811,17 +1130,17 @@ const handleSendMessage = async (content: string) => { contact.lastMessageTime = newMessage.time } + // 发送消息后滚动到底部 + nextTick(() => { + scrollToBottom() + }) + // 模拟发送到服务器 try { // 模拟网络延迟 await new Promise(resolve => setTimeout(resolve, 300)) // 模拟成功响应 - console.log('模拟发送消息成功:', { - chatId: activeContactId.value, - content, - messageType: 'text' - }) // 模拟对方回复(随机回复) setTimeout(() => { @@ -868,6 +1187,7 @@ const simulateReply = (chatId: string) => { type: 'text', content: randomReply, senderName: '对方', + senderId: '2000000000', // 对方用户ID avatar: 'https://avatars.githubusercontent.com/u/38358644', // 使用示例头像 time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), isOwn: false, @@ -884,20 +1204,14 @@ const simulateReply = (chatId: string) => { contact.unreadCount = (contact.unreadCount || 0) + 1 } + // 收到回复后滚动到底部 nextTick(() => { scrollToBottom() }) } -const scrollToBottom = () => { - if (messagesContainer.value) { - messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight - } -} - -const previewImage = (src: string) => { +const previewImage = (_src: string) => { // TODO: 实现图片预览功能 - console.log('预览图片:', src) } // 获取文件图标 @@ -914,7 +1228,6 @@ const getFileIcon = (type: string) => { // 下载文件 const downloadFile = (message: any) => { // 这里可以实现文件下载逻辑 - console.log('下载文件:', message.fileName) // 可以创建一个临时的下载链接 if (message.fileUrl) { const link = document.createElement('a') @@ -939,7 +1252,6 @@ const closeDetailsPanel = () => { // 新增事件处理方法 const handleEmoji = () => { - console.log('选择表情') // TODO: 实现表情选择功能 } @@ -960,6 +1272,7 @@ const handleImage = (imageData: any) => { fileName: imageData.name || '图片', fileSize: imageData.size?.toString() || '0', senderName: userStore.user?.profile?.realName || userStore.user?.nickname || '我', + senderId: userStore.user?.id?.toString() || '', // 发送者ID avatar: userStore.user?.avatar || '', time: formatTime(new Date().toISOString()), isOwn: true, @@ -991,6 +1304,7 @@ const handleImage = (imageData: any) => { type: 'text', content: '收到图片了!', senderName: '对方', + senderId: '2000000000', // 对方用户ID avatar: 'https://avatars.githubusercontent.com/u/38358644', time: formatTime(new Date().toISOString()), isOwn: false, @@ -1033,6 +1347,7 @@ const handleFile = (fileData: any) => { fileSize: fileData.size.toString(), fileType: fileData.type, senderName: userStore.user?.profile?.realName || userStore.user?.nickname || '我', + senderId: userStore.user?.id?.toString() || '', // 发送者ID avatar: userStore.user?.avatar || '', time: formatTime(new Date().toISOString()), isOwn: true, @@ -1064,6 +1379,7 @@ const handleFile = (fileData: any) => { type: 'text', content: '收到文件了!', senderName: '对方', + senderId: '2000000000', // 对方用户ID avatar: 'https://avatars.githubusercontent.com/u/38358644', time: formatTime(new Date().toISOString()), isOwn: false, @@ -1198,8 +1514,8 @@ onMounted(() => { } .avatar-placeholder { - width: 70px; - height: 70px; + width: 50px; + height: 50px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; @@ -1234,7 +1550,7 @@ onMounted(() => { .contact-header { display: flex; align-items: center; - margin-bottom: 10px; + margin-bottom: 2px; color: #3F3F44; } @@ -1455,6 +1771,7 @@ onMounted(() => { .message-avatar { margin-right: 8px; flex-shrink: 0; + position: relative; } .message-own .message-avatar { @@ -1500,7 +1817,20 @@ onMounted(() => { .message-sender { font-size: 12px; color: #666; - margin-bottom: 4px; + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 6px; +} + +.teacher-label { + background: #E2F5FF; + color: #0088D1; + border: 1px solid #0088D1; + padding: 0 5px; + border-radius: 2px; + font-size: 10px; + font-weight: 500; } .message-bubble { @@ -1995,6 +2325,7 @@ onMounted(() => { border-radius: 50%; animation: spin 1s linear infinite; } + .actions-section { margin: 0 20px; display: flex; @@ -2108,4 +2439,175 @@ onMounted(() => { max-width: 75%; } } + +/* 成员弹框样式 - 符合项目风格 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.member-modal { + background: #ffffff; + border-radius: 8px; + width: 380px; + max-width: 90vw; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + animation: modalSlideIn 0.2s ease-out; + border: 1px solid #e6e6e6; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #e6e6e6; + background: #fafafa; + border-radius: 8px 8px 0 0; +} + +.modal-header h3 { + margin: 0; + font-size: 16px; + font-weight: 500; + color: #333; +} + +.close-btn { + background: none; + border: none; + font-size: 20px; + color: #999; + cursor: pointer; + padding: 4px; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s; +} + +.close-btn:hover { + background: #f0f0f0; + color: #666; +} + +.modal-content { + padding: 20px; +} + +.member-profile { + display: flex; + align-items: center; + margin-bottom: 20px; + padding: 12px; + background: #f8f9fa; + border-radius: 6px; + border: 1px solid #e6e6e6; +} + +.member-avatar-large { + width: 50px; + height: 50px; + border-radius: 50%; + overflow: hidden; + margin-right: 12px; + flex-shrink: 0; + border: 2px solid #e6e6e6; +} + +.member-avatar-large img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.member-details h4 { + margin: 0 0 6px 0; + font-size: 15px; + font-weight: 500; + color: #333; +} + +.member-details .member-role { + margin: 0; + font-size: 12px; + color: #666; + background: #e6e6e6; + padding: 2px 6px; + border-radius: 10px; + display: inline-block; +} + +.modal-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.modal-btn { + padding: 8px 16px; + border: 1px solid #e6e6e6; + border-radius: 4px; + font-size: 13px; + font-weight: 400; + cursor: pointer; + transition: all 0.2s; + min-width: 80px; + background: #ffffff; +} + +.modal-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.modal-btn.mute-btn { + background: #ffffff; + color: #ff4d4f; + border-color: #ff4d4f; +} + +.modal-btn.mute-btn:hover:not(:disabled) { + background: #ff4d4f; + color: white; +} + +.modal-btn.remove-btn { + background: #ffffff; + color: #666; + border-color: #d9d9d9; +} + +.modal-btn.remove-btn:hover:not(:disabled) { + background: #f5f5f5; + color: #333; +} + +/* 群成员列表样式 - 保持原有样式,只添加点击效果 */ +.member-item { + cursor: pointer; +}