diff --git a/src/api/modules/auth.ts b/src/api/modules/auth.ts index f592a6d..4848955 100644 --- a/src/api/modules/auth.ts +++ b/src/api/modules/auth.ts @@ -77,7 +77,7 @@ export class AuthApi { message: actualMessage || '登录成功', data: { user: { - id: 1, // 真实API没有返回用户ID,使用默认值 + id: "1", // 真实API没有返回用户ID,使用默认值 email: data.email || '', phone: data.phone || '', username: data.email || data.phone || '', @@ -129,7 +129,7 @@ export class AuthApi { message: actualMessage || '登录成功', data: { user: { - id: 1, + id: "1", email: data.email || '', phone: data.phone || '', username: data.phone || data.email?.split('@')[0] || 'user', @@ -232,7 +232,7 @@ export class AuthApi { code: 200, message: actualMessage || '注册成功', data: { - id: Date.now(), // 临时ID + id: Date.now().toString(), // 临时ID username: registerData.studentNumber, email: data.email || '', phone: data.phone || '', @@ -339,7 +339,7 @@ export class AuthApi { } const convertedUser = { - id: parseInt(baseInfo.id) || 0, + id: baseInfo.id, // 直接使用字符串ID,避免parseInt精度问题 username: baseInfo.username, email: baseInfo.email, phone: baseInfo.phone, diff --git a/src/api/modules/chat.ts b/src/api/modules/chat.ts index 5e1adf3..8a09d42 100644 --- a/src/api/modules/chat.ts +++ b/src/api/modules/chat.ts @@ -33,7 +33,7 @@ export interface ChatMessage { senderName: string senderAvatar?: string content: string - messageType: 'text' | 'image' | 'file' | 'system' + messageType: number // 0=文本, 1=图片, 2=文件, 3=系统消息 timestamp: string isRead: boolean replyTo?: string @@ -42,6 +42,8 @@ export interface ChatMessage { fileUrl?: string // 文件URL fileSize?: number // 文件大小 fileType?: string // 文件类型 + fileName?: string // 文件名 + createTime?: string // 创建时间 } // 群聊成员接口类型定义 @@ -56,11 +58,13 @@ export interface ChatMember { username: string // 可选字段 chatId?: string - role?: 'admin' | 'member' + role: 0 | 1 | 2 // 0=群主, 1=管理员, 2=成员 joinTime?: string isOnline?: boolean status?: number // 成员状态 lastActiveTime?: string // 最后活跃时间 + izMuted: 0 | 1 // 是否被禁言: 0=未禁言, 1=已禁言 + izNotDisturb?: 0 | 1 // 是否免打扰: 0=未设置, 1=已设置 } // 我的会话列表响应类型 @@ -142,19 +146,32 @@ export const ChatApi = { /** * 发送消息 - * POST /aiol/aiolChat/send + * POST /aiol/aiolChatMessage/send * 根据接口文档添加发送消息功能 */ sendMessage: (data: { - chatId: string + chat_id: string content: string - messageType: 'text' | 'image' | 'file' + messageType: number // 0=文本,1=图片,2=文件 replyTo?: string fileUrl?: string fileSize?: number fileType?: string + fileName?: string }): Promise> => { - return ApiRequest.post('/aiol/aiolChat/send', data) + // 转换参数名以匹配服务器期望的格式 + const serverData = { + chat_id: data.chat_id, + content: data.content, + message_type: data.messageType, // 服务器可能期望 message_type + replyTo: data.replyTo, + fileUrl: data.fileUrl, + fileSize: data.fileSize, + fileType: data.fileType, + fileName: data.fileName + } + console.log('🔄 转换后的服务器数据:', serverData) + return ApiRequest.post('/aiol/aiolChatMessage/send', serverData) }, @@ -233,5 +250,46 @@ export const ChatApi = { */ unmuteMember: (chatId: string, userId: string): Promise> => { return ApiRequest.post(`/aiol/aiolChat/${chatId}/unmute_member/${userId}`) + }, + + /** + * 开启免打扰 + * POST /aiol/aiolChat/{chatId}/enable_not_disturb/{userId} + */ + enableNotDisturb: (chatId: string, userId: string): Promise> => { + return ApiRequest.post(`/aiol/aiolChat/${chatId}/enable_not_disturb/${userId}`) + }, + + /** + * 关闭免打扰 + * POST /aiol/aiolChat/{chatId}/disable_not_disturb/{userId} + */ + disableNotDisturb: (chatId: string, userId: string): Promise> => { + return ApiRequest.post(`/aiol/aiolChat/${chatId}/disable_not_disturb/${userId}`) + }, + + /** + * 更新最后读取消息ID + * POST /aiol/aiolChat/{chatId}/update_last_read/{messageId} + * 更新当前用户在指定会话中的最后读取消息ID + */ + updateLastRead: (chatId: string, messageId: string): Promise> => { + return ApiRequest.post(`/aiol/aiolChat/${chatId}/update_last_read/${messageId}`) + }, + + /** + * 通用文件上传 + * POST /sys/common/upload + * 上传文件并返回文件URL + */ + uploadFile: (file: File): Promise> => { + const formData = new FormData() + formData.append('file', file) + + return ApiRequest.post('/sys/common/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) } } diff --git a/src/api/types.ts b/src/api/types.ts index 1a51617..2b267fd 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -29,7 +29,7 @@ export interface PaginationResponse { // 用户相关类型 export interface User { - id: number + id: string // 改为字符串类型,避免大整数精度问题 username: string email: string phone?: string @@ -218,12 +218,12 @@ export interface CourseListRequest { } export interface CourseCategory { - id: number + id: string name: string slug: string description?: string icon?: string - parentId?: number + parentId?: string children?: CourseCategory[] } @@ -359,7 +359,7 @@ export interface BackendCourseDetailResponse { } export interface Instructor { - id: number + id: string name: string title: string bio: string diff --git a/src/components/teacher/ClassManagement.vue b/src/components/teacher/ClassManagement.vue index 7a3836b..4a73fcc 100644 --- a/src/components/teacher/ClassManagement.vue +++ b/src/components/teacher/ClassManagement.vue @@ -288,6 +288,57 @@ :radio-options="importRadioOptions" radio-field="updateMode" import-type="student" template-name="student_import_template.xlsx" @success="handleImportSuccess" @template-download="handleTemplateDownload" /> + + + + + +
+ + + + + 搜索 + + + 清空 + +
+ + + + +
+
+ 共找到 {{ filteredLibraryStudents.length }} 名学员 + + 已选择 {{ selectedLibraryStudents.length }} 名 + +
+ + +
+ + +
+
@@ -295,7 +346,7 @@ import { ref, onMounted, h, computed, watch } from 'vue' import TeachCourseApi, { ClassApi } from '@/api/modules/teachCourse' import { useRouter, useRoute } from 'vue-router' -import { AddCircleOutline, SettingsOutline, QrCode } from '@vicons/ionicons5' +import { AddCircleOutline, SettingsOutline, QrCode, SearchOutline } from '@vicons/ionicons5' import { NDataTable, NButton, @@ -359,6 +410,16 @@ interface ClassItem { createTime: string } +// 学员库数据类型定义 +interface LibraryStudentItem { + id: string + realName: string + studentNumber: string + className: string + school: string + createTime: string +} + // 表单数据类型 interface FormData { studentName: string @@ -386,8 +447,15 @@ const showTransferModal = ref(false) const showAddClassModal = ref(false) const showManageClassModal = ref(false) const showImportModal = ref(false) +const showStudentLibraryModal = ref(false) const selectedTargetClass = ref('') const currentTransferStudent = ref(null) + +// 学员库相关数据 +const librarySearchKeyword = ref('') +const libraryStudents = ref([]) +const selectedLibraryStudents = ref([]) +const libraryLoading = ref(false) const formRef = ref(null) const classFormRef = ref(null) const isEditMode = ref(false) @@ -525,6 +593,22 @@ const classSelectOptions = computed(() => // 班级管理列表(使用主数据源) const classList = computed(() => masterClassList.value) +// 学员库相关计算属性 +const filteredLibraryStudents = computed(() => { + if (!librarySearchKeyword.value.trim()) { + return libraryStudents.value + } + + const keyword = librarySearchKeyword.value.trim().toLowerCase() + return libraryStudents.value.filter(student => + student.realName.toLowerCase().includes(keyword) || + student.studentNumber.toLowerCase().includes(keyword) || + student.school.toLowerCase().includes(keyword) || + student.className.toLowerCase().includes(keyword) + ) +}) + + // 表格列定义 const columns: DataTableColumns = [ { @@ -664,6 +748,56 @@ const columns: DataTableColumns = [ } ] +// 学员库表格列定义 +const libraryColumns: DataTableColumns = [ + { + type: 'selection' + }, + { + title: '姓名', + key: 'realName', + width: 120, + align: 'center' + }, + { + title: '账号', + key: 'studentNumber', + width: 140, + align: 'center' + }, + { + title: '班级', + key: 'className', + width: 150, + align: 'center', + render: (row: LibraryStudentItem) => { + // 使用辅助函数获取班级名称数组 + const classNames = formatClassNames(row.className || '') + // 渲染班级名称,支持多行显示 + return h('div', { + class: 'class-cell' + }, classNames.map((name, index) => + h('div', { + key: index, + class: 'class-cell-item' + }, name) + )) + } + }, + { + title: '所在学院', + key: 'school', + width: 200, + align: 'center' + }, + { + title: '加入时间', + key: 'createTime', + width: 140, + align: 'center' + } +] + // 表格数据 const data = ref([]) const loading = ref(false) @@ -686,6 +820,22 @@ const pagination = ref({ } }) +// 学员库分页配置 +const libraryPagination = ref({ + page: 1, + pageSize: 10, + showSizePicker: true, + pageSizes: [10, 20, 50], + itemCount: computed(() => filteredLibraryStudents.value.length), + onChange: (page: number) => { + libraryPagination.value.page = page + }, + onUpdatePageSize: (pageSize: number) => { + libraryPagination.value.pageSize = pageSize + libraryPagination.value.page = 1 + } +}) + // 操作处理函数 const handleTransfer = (row: StudentItem) => { currentTransferStudent.value = row @@ -871,7 +1021,34 @@ const confirmBatchTransfer = async () => { } const handleDelete = (row: StudentItem) => { - message.warning(`移除学员:${row.studentName}`) + dialog.warning({ + title: '确认移除', + content: `确定要移除学员"${row.studentName}"吗?`, + positiveText: '确定', + negativeText: '取消', + onPositiveClick: async () => { + try { + const currentClassId = props.classId || selectedDepartment.value + if (!currentClassId) { + message.error('班级ID为空,无法移除学员') + return + } + + const response = await ClassApi.removeStudent(String(currentClassId), row.id) + + if (response.data && (response.data.success || response.data.code === 200)) { + message.success(`已移除学员"${row.studentName}"`) + // 重新加载数据 + loadData(currentClassId) + } else { + message.error(response.data?.message || '移除失败') + } + } catch (error) { + console.error('移除学员失败:', error) + message.error('移除学员失败,请重试') + } + } + }) } // 查看学习进度处理函数(student 模式) @@ -1117,9 +1294,8 @@ const handleAddStudentSelect = (key: string) => { // 手动添加,打开现有的添加学员弹窗 openAddModal() } else if (key === 'library') { - // 学员库添加,先保留功能,后续实现 - message.info('学员库添加功能待开发,敬请期待') - console.log('学员库添加功能将在后续版本中实现') + // 学员库添加,打开学员库选择弹窗 + openStudentLibraryModal() } } @@ -1278,7 +1454,7 @@ const loadData = async (classId?: string | number | null) => { // 转换API响应数据为组件需要的格式 const studentsData = response.data.result || [] const transformedData: StudentItem[] = studentsData.map((student: any) => ({ - id: student.id || '', + id: student.studentId || student.id || '', // 使用studentId作为主要ID studentName: student.realname || student.username || '未知姓名', accountNumber: student.studentId || student.username || '', className: student.classId || '未分配班级', // 班级ID,后续转换为班级名称 @@ -1342,6 +1518,158 @@ const clearSearch = () => { pagination.value.page = 1 } +// 学员库相关方法 +const openStudentLibraryModal = async () => { + showStudentLibraryModal.value = true + selectedLibraryStudents.value = [] + librarySearchKeyword.value = '' + await loadLibraryStudents() +} + +const closeStudentLibraryModal = () => { + showStudentLibraryModal.value = false + selectedLibraryStudents.value = [] + librarySearchKeyword.value = '' + libraryStudents.value = [] +} + +const loadLibraryStudents = async () => { + libraryLoading.value = true + try { + console.log('🚀 开始加载学员库数据...') + + // 先加载班级列表 + await loadClassList() + + if (masterClassList.value.length === 0) { + libraryStudents.value = [] + return + } + + // 获取所有班级的学员数据 + const allStudents: LibraryStudentItem[] = [] + + for (const classItem of masterClassList.value) { + try { + const response = await ClassApi.getClassStudents(classItem.id) + const studentsData = response.data.result || [] + + const transformedStudents: LibraryStudentItem[] = studentsData.map((student: any) => ({ + id: student.id || student.studentId || '', + realName: student.realname || student.username || '未知姓名', + studentNumber: student.studentId || student.username || '', + className: classItem.id, // 使用班级ID,后续会转换为班级名称 + school: student.college || student.department || '未分配学院', + createTime: student.createTime ? new Date(student.createTime).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }).replace(/\//g, '.').replace(',', '') : '未知时间' + })) + + allStudents.push(...transformedStudents) + } catch (error) { + console.error(`❌ 加载班级 ${classItem.className} 的学员数据失败:`, error) + } + } + + libraryStudents.value = allStudents + console.log(`✅ 成功加载学员库数据,共 ${allStudents.length} 名学员`) + } catch (error) { + console.error('❌ 加载学员库数据失败:', error) + message.error('加载学员库数据失败,请重试') + libraryStudents.value = [] + } finally { + libraryLoading.value = false + } +} + +const handleLibrarySearch = () => { + libraryPagination.value.page = 1 +} + +const clearLibrarySearch = () => { + librarySearchKeyword.value = '' + libraryPagination.value.page = 1 +} + +const handleConfirmLibrarySelection = async () => { + if (selectedLibraryStudents.value.length === 0) { + message.warning('请先选择要添加的学员') + return + } + + try { + const currentClassId = props.classId || selectedDepartment.value + if (!currentClassId) { + message.error('班级ID为空,无法添加学员') + return + } + + // 获取选中的学员数据 + const selectedStudents = libraryStudents.value.filter(student => + selectedLibraryStudents.value.includes(student.id) + ) + + console.log('🚀 开始添加学员到班级:', { + 目标班级ID: currentClassId, + 选中学员数量: selectedStudents.length, + 选中学员: selectedStudents.map(s => ({ id: s.id, name: s.realName, studentNumber: s.studentNumber })) + }) + + // 批量添加学员到班级 + let successCount = 0 + let failCount = 0 + + for (const student of selectedStudents) { + try { + // 构建添加学员的API请求参数 + const payload = { + realName: student.realName, + studentNumber: student.studentNumber, + password: '123456', // 默认密码 + school: student.school, + classId: String(currentClassId) + } + + console.log('📝 添加学员API请求参数:', payload) + + // 调用创建学员API + const response = await ClassApi.createdStudents(payload) + + if (response.data && (response.data.success || response.data.code === 200)) { + successCount++ + console.log(`✅ 成功添加学员: ${student.realName}`) + } else { + failCount++ + console.error(`❌ 添加学员失败: ${student.realName}`, response.data?.message) + } + } catch (error) { + failCount++ + console.error(`❌ 添加学员异常: ${student.realName}`, error) + } + } + + if (successCount > 0) { + message.success(`成功添加 ${successCount} 名学员到班级${failCount > 0 ? `,${failCount} 名添加失败` : ''}`) + } else { + message.error('添加学员失败,请重试') + return + } + + // 关闭弹窗 + closeStudentLibraryModal() + + // 重新加载班级学员数据 + loadData(currentClassId) + } catch (error) { + console.error('添加学员失败:', error) + message.error('添加学员失败,请重试') + } +} + // 监听班级ID变化,重新加载数据 watch( () => props.classId, @@ -1759,4 +2087,35 @@ defineExpose({ .custom-empty .n-empty { margin: 0; } + +/* 学员库弹窗样式 */ +.search-section { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 0; +} + +.student-list-section { + margin-top: 16px; +} + +.list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding: 0 4px; +} + +.total-count { + font-size: 14px; + color: #666; +} + +.selected-count { + font-size: 14px; + color: #1890ff; + font-weight: 500; +} \ No newline at end of file diff --git a/src/components/teacher/StudentLibraryModal.vue b/src/components/teacher/StudentLibraryModal.vue new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/components/teacher/StudentLibraryModal.vue @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/views/AICompanion.vue b/src/views/AICompanion.vue index 65c00ac..72af8a3 100644 --- a/src/views/AICompanion.vue +++ b/src/views/AICompanion.vue @@ -1210,7 +1210,7 @@ const loadCourseDetail = async () => { if (course.value) { if (!course.value.instructor?.name) { course.value.instructor = { - id: 1, + id: "1", name: 'DeepSeek技术学院', title: '讲师', bio: '', @@ -1292,7 +1292,7 @@ const loadMockCourseData = () => { title: 'DeepSeek办公自动化职业岗位标准课程', description: '本课程将帮助您掌握DeepSeek的基本使用方法,了解办公自动化职业岗位标准,提高教学质量和效率,获得实际工作技能。', instructor: { - id: 1, + id: "1", name: 'DeepSeek技术学院', title: '讲师', bio: '专注于AI技术应用与教学', @@ -1311,7 +1311,7 @@ const loadMockCourseData = () => { price: 0, originalPrice: 299, category: { - id: 1, + id: "1", name: 'AI技术', slug: 'ai-technology', description: 'AI技术', @@ -1679,7 +1679,7 @@ const initializeMockState = () => { // 模拟用户已登录 if (!userStore.isLoggedIn) { userStore.user = { - id: 1, + id: "1", username: 'testuser', email: 'test@example.com', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80', diff --git a/src/views/CourseDetail.vue b/src/views/CourseDetail.vue index 4862871..87a8bc6 100644 --- a/src/views/CourseDetail.vue +++ b/src/views/CourseDetail.vue @@ -793,19 +793,19 @@ const activeTab = ref('intro') // 讲师数据 const instructors = ref([ { - id: 1, + id: "1", name: '汪波', title: '教授', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80' }, { - id: 2, + id: "2", name: '汪波', title: '教授', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80' }, { - id: 3, + id: "3", name: '汪波', title: '教授', avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80' @@ -869,7 +869,7 @@ const loadCourseDetail = async () => { if (course.value) { if (!course.value.instructor?.name) { course.value.instructor = { - id: 1, + id: "1", name: 'DeepSeek技术学院', title: '讲师', bio: '', @@ -1408,7 +1408,7 @@ const initializeMockState = () => { // 模拟用户已登录 if (!userStore.isLoggedIn) { userStore.user = { - id: 1, + id: "1", username: 'testuser', email: 'test@example.com', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80', diff --git a/src/views/CourseDetailEnrolled.vue b/src/views/CourseDetailEnrolled.vue index d34e4e4..4dd80e4 100644 --- a/src/views/CourseDetailEnrolled.vue +++ b/src/views/CourseDetailEnrolled.vue @@ -1247,7 +1247,7 @@ const initializeEnrolledState = () => { // 模拟用户已登录 if (!userStore.isLoggedIn) { userStore.user = { - id: 1, + id: "1", username: 'testuser', email: 'test@example.com', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80', diff --git a/src/views/CourseExchanged.vue b/src/views/CourseExchanged.vue index e794533..6c134d5 100644 --- a/src/views/CourseExchanged.vue +++ b/src/views/CourseExchanged.vue @@ -2077,7 +2077,7 @@ const loadCourseDetail = async () => { if (course.value) { if (!course.value.instructor?.name) { course.value.instructor = { - id: 1, + id: "1", name: 'DeepSeek技术学院', title: '讲师', bio: '', diff --git a/src/views/CourseStudy.vue b/src/views/CourseStudy.vue index 0658cb8..fad48f3 100644 --- a/src/views/CourseStudy.vue +++ b/src/views/CourseStudy.vue @@ -593,13 +593,13 @@ const setDefaultCourseInfo = () => { description: '

本课程将带你从零开始学习C语言程序设计,掌握编程基础知识。

', content: '详细的C语言课程内容', category: { - id: 3, + id: "3", name: '信息技术', slug: 'it', description: '信息技术相关课程' }, instructor: { - id: 4, + id: "4", name: '教师', title: 'C语言专家', bio: '资深C语言开发工程师', diff --git a/src/views/Login.vue b/src/views/Login.vue index 9156664..fb1e31e 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -285,7 +285,7 @@ const handleLogin = async () => { // 如果获取用户信息失败,创建基本用户信息 console.warn('⚠️ 获取用户信息失败,使用基本信息') const basicUser = { - id: 1, + id: "1", email: isPhone ? '' : formData.studentId, phone: isPhone ? formData.studentId : '', username: formData.studentId, @@ -303,7 +303,7 @@ const handleLogin = async () => { // 如果获取用户信息异常,创建基本用户信息 console.warn('⚠️ 获取用户信息异常,使用基本信息:', userInfoError) const basicUser = { - id: 1, + id: "1", email: isPhone ? '' : formData.studentId, phone: isPhone ? formData.studentId : '', username: formData.studentId, diff --git a/src/views/TeacherDetail.vue b/src/views/TeacherDetail.vue index 0fab5ae..0f3e5ab 100644 --- a/src/views/TeacherDetail.vue +++ b/src/views/TeacherDetail.vue @@ -219,13 +219,13 @@ const loadCourses = async () => { totalLessons: 54, level: 'beginner', language: 'zh-CN', - category: { id: 1, name: '教育培训', slug: 'education-training' }, + category: { id: "1", name: '教育培训', slug: 'education-training' }, tags: ['心理学', '教育', '基础'], skills: ['心理分析', '教育理论'], requirements: ['无特殊要求'], objectives: ['掌握教育心理学基础理论'], instructor: { - id: 1, + id: "1", name: '汪波', avatar: '/images/Teachers/师资力量1.png', title: '云南师范大学教授', @@ -255,13 +255,13 @@ const loadCourses = async () => { totalLessons: 42, level: 'intermediate', language: 'zh-CN', - category: { id: 2, name: '技术应用', slug: 'tech-application' }, + category: { id: "2", name: '技术应用', slug: 'tech-application' }, tags: ['教育技术', '多媒体', '在线教育'], skills: ['多媒体制作', '在线教学'], requirements: ['基础计算机操作'], objectives: ['掌握现代教育技术应用'], instructor: { - id: 1, + id: "1", name: '汪波', avatar: '/images/Teachers/师资力量1.png', title: '云南师范大学教授', @@ -291,13 +291,13 @@ const loadCourses = async () => { totalLessons: 68, level: 'advanced', language: 'zh-CN', - category: { id: 3, name: '课程设计', slug: 'course-design' }, + category: { id: "3", name: '课程设计', slug: 'course-design' }, tags: ['课程设计', '教学开发', '教育'], skills: ['课程规划', '教学设计'], requirements: ['教育理论基础'], objectives: ['掌握课程设计方法'], instructor: { - id: 1, + id: "1", name: '汪波', avatar: '/images/Teachers/师资力量1.png', title: '云南师范大学教授', @@ -327,13 +327,13 @@ const loadCourses = async () => { totalLessons: 36, level: 'intermediate', language: 'zh-CN', - category: { id: 4, name: '研究方法', slug: 'research-methods' }, + category: { id: "4", name: '研究方法', slug: 'research-methods' }, tags: ['研究方法', '教育', '学术'], skills: ['研究设计', '数据分析'], requirements: ['统计学基础'], objectives: ['掌握教育研究方法'], instructor: { - id: 1, + id: "1", name: '汪波', avatar: '/images/Teachers/师资力量1.png', title: '云南师范大学教授', @@ -363,13 +363,13 @@ const loadCourses = async () => { totalLessons: 28, level: 'beginner', language: 'zh-CN', - category: { id: 5, name: '心理辅导', slug: 'psychological-counseling' }, + category: { id: "5", name: '心理辅导', slug: 'psychological-counseling' }, tags: ['心理辅导', '学生', '技巧'], skills: ['心理咨询', '沟通技巧'], requirements: ['心理学基础'], objectives: ['掌握心理辅导技巧'], instructor: { - id: 1, + id: "1", name: '汪波', avatar: '/images/Teachers/师资力量1.png', title: '云南师范大学教授', @@ -399,13 +399,13 @@ const loadCourses = async () => { totalLessons: 40, level: 'advanced', language: 'zh-CN', - category: { id: 6, name: '教育评估', slug: 'education-assessment' }, + category: { id: "6", name: '教育评估', slug: 'education-assessment' }, tags: ['教育评估', '测量', '技术'], skills: ['评估设计', '测量技术'], requirements: ['教育统计学'], objectives: ['掌握教育评估方法'], instructor: { - id: 1, + id: "1", name: '汪波', avatar: '/images/Teachers/师资力量1.png', title: '云南师范大学教授', diff --git a/src/views/teacher/course/CourseDetail.vue b/src/views/teacher/course/CourseDetail.vue index f638f5c..fd97ea9 100644 --- a/src/views/teacher/course/CourseDetail.vue +++ b/src/views/teacher/course/CourseDetail.vue @@ -405,7 +405,7 @@ const loadCourseCategoryFromManagementAPI = async () => { // 根据ID匹配分类名称 const categoryNames = categoryIds.map((id: number) => { - const category = categoryResponse.data.find(cat => cat.id === id) + const category = categoryResponse.data.find(cat => cat.id === String(id)) return category ? category.name : `未知分类${id}` }).filter(Boolean) diff --git a/src/views/teacher/message/components/NotificationMessages.vue b/src/views/teacher/message/components/NotificationMessages.vue index 20517d1..c6e2896 100644 --- a/src/views/teacher/message/components/NotificationMessages.vue +++ b/src/views/teacher/message/components/NotificationMessages.vue @@ -22,9 +22,11 @@
-
+
@@ -42,6 +44,15 @@ {{ contact.name }} ({{ contact.memberCount || 0 }}人) + + + + + + + +
@@ -74,7 +85,7 @@

{{ activeContact?.name }} ({{ activeContact?.memberCount || 0 -}}人) + }}人)

@@ -95,8 +106,51 @@
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
@@ -109,13 +163,8 @@
- -
- {{ message.dateText }} -
- - -
+ +
{{ message.time }}
@@ -132,7 +181,8 @@
-
+ +
{{ message.senderName }} 讲师
@@ -259,8 +309,9 @@
消息免打扰
- - + +
@@ -355,19 +406,26 @@

{{ selectedMember?.realname }}

-

讲师

-

学员

+

+ 群主 + 管理员 + 成员 + (讲师) + (已禁言) + (免打扰) + (本人) +

@@ -400,6 +458,7 @@ interface Contact { memberCount?: number izAllMuted?: boolean | number // 全员禁言状态,支持布尔值和数字(1/0) showLabel?: boolean | number // 是否显示教师标签,支持布尔值和数字(1/0) + izNotDisturb?: 0 | 1 // 免打扰状态:0=否,1=是 } // 群聊成员类型定义 @@ -412,9 +471,13 @@ interface ChatMember { email: string realname: string username: string + // 角色和权限相关字段 + role: 0 | 1 | 2 // 0=群主, 1=管理员, 2=成员 + izMuted: 0 | 1 // 是否被禁言: 0=未禁言, 1=已禁言 + izNotDisturb?: 0 | 1 // 是否免打扰: 0=否, 1=是 + lastReadMsgId?: string | null // 最后已读消息ID // 可选字段 chatId?: string - role?: 'admin' | 'member' joinTime?: string isOnline?: boolean status?: number // 成员状态 @@ -433,8 +496,8 @@ interface Message { time: string isOwn: boolean isRead: boolean // 消息是否已读 - showDateDivider?: boolean - dateText?: string + showTime?: boolean // 是否显示时间 + showSender?: boolean // 是否显示发送者 fileName?: string fileSize?: string fileUrl?: string @@ -535,8 +598,10 @@ const shouldShowViewMore = computed(() => { }) // 生命周期钩子 -onMounted(() => { - loadContacts() +onMounted(async () => { + await loadContacts() + await loadAllGroupNotDisturbStatus() + await loadAllGroupLastMessages() loadTeacherList() }) @@ -593,10 +658,24 @@ const loadContacts = async () => { isOnline: chat.isOnline, memberCount: chat.memberCount || (contactType === 'group' ? 0 : undefined), izAllMuted: chat.izAllMuted === 1, - showLabel: chat.showLabel === 1 + showLabel: chat.showLabel === 1, + izNotDisturb: chat.izNotDisturb || 0 // 添加免打扰状态字段 } }) + // 打印联系人列表 + console.log('📋 联系人列表:', { + totalCount: contacts.value.length, + contacts: contacts.value.map(contact => ({ + id: contact.id, + name: contact.name, + type: contact.type, + memberCount: contact.memberCount, + isOnline: contact.isOnline, + unreadCount: contact.unreadCount + })) + }) + // 如果是群聊且没有memberCount,尝试获取群成员数量 for (const contact of contacts.value) { @@ -622,6 +701,13 @@ const loadGroupMemberCount = async (chatId: string) => { if (response.data && response.data.success && response.data.result) { const memberCount = response.data.result.length + // 打印群聊成员数量 + console.log('📊 群聊成员数量:', { + chatId, + memberCount, + contactName: contacts.value.find(c => c.id === chatId)?.name || '未知' + }) + // 更新对应联系人的成员数量 const contact = contacts.value.find((c: Contact) => c.id === chatId) if (contact) { @@ -641,6 +727,25 @@ const loadGroupMembers = async (chatId: string) => { if (response.data && response.data.success) { groupMembers.value = response.data.result || [] + // 打印群成员列表 - 包含角色和禁言状态 + console.log('👥 群成员列表:', { + chatId, + memberCount: groupMembers.value.length, + members: groupMembers.value.map(member => ({ + id: member.id, + realname: member.realname, + username: member.username, + isTeacher: member.isTeacher, + role: member.role, // 0=群主, 1=管理员, 2=成员 + roleText: member.role === 0 ? '群主' : member.role === 1 ? '管理员' : '成员', + izMuted: member.izMuted, // 0=未禁言, 1=已禁言 + isMuted: member.izMuted === 1, + avatar: member.avatar, + email: member.email, + phone: member.phone + })) + }) + // 从联系人数据中获取群组名称和禁言状态 const contact = contacts.value.find((c: Contact) => c.id === chatId) const groupName = contact ? contact.name : '暂无' @@ -651,7 +756,10 @@ const loadGroupMembers = async (chatId: string) => { memberCount: groupMembers.value.length } + // 注意:免打扰状态已在页面加载时通过 loadAllGroupNotDisturbStatus 获取 + } else { + console.warn('⚠️ 获取群成员失败:', response.data) } } catch (error) { console.error('获取群成员失败:', error) @@ -663,12 +771,140 @@ const loadGroupMembers = async (chatId: string) => { const handleMemberSearch = () => { isSearchingMembers.value = true - // 模拟搜索延迟 + // 搜索延迟 setTimeout(() => { isSearchingMembers.value = false }, 300) } +// 获取所有群聊的免打扰状态 +const loadAllGroupNotDisturbStatus = async () => { + try { + const currentUserId = userStore.user?.id + if (!currentUserId) return + + console.log('🔄 开始加载所有群聊免打扰状态...') + + // 并行获取所有群聊的成员信息 + const groupChats = contacts.value.filter(contact => contact.type === 'group') + const promises = groupChats.map(async (contact) => { + try { + const response = await ChatApi.getChatMembers(contact.id) + if (response.data && response.data.success) { + const members = response.data.result || [] + const currentUserMember = members.find((member: any) => member.id === currentUserId) + + if (currentUserMember && currentUserMember.izNotDisturb !== undefined) { + contact.izNotDisturb = currentUserMember.izNotDisturb + console.log('✅ 群聊免打扰状态:', { + chatId: contact.id, + chatName: contact.name, + izNotDisturb: contact.izNotDisturb + }) + } + } + } catch (error) { + console.warn('⚠️ 获取群聊免打扰状态失败:', contact.id, error) + } + }) + + await Promise.all(promises) + console.log('🎉 所有群聊免打扰状态加载完成') + + } catch (error) { + console.error('❌ 加载所有群聊免打扰状态失败:', error) + } +} + +// 获取所有群聊的最后消息 +const loadAllGroupLastMessages = async () => { + try { + console.log('🔄 开始加载所有群聊最后消息...') + + // 并行获取所有群聊的最后消息 + const groupChats = contacts.value.filter(contact => contact.type === 'group') + const promises = groupChats.map(async (contact) => { + try { + const response = await ChatApi.getChatMessages(contact.id) + if (response.data && response.data.success && response.data.result.length > 0) { + const messages = response.data.result + const lastMessage = messages[messages.length - 1] + + // 根据消息类型生成显示内容 + let displayContent = '' + if (lastMessage.messageType === 0) { + // 文本消息 + displayContent = lastMessage.content + } else if (lastMessage.messageType === 1) { + // 图片消息 + displayContent = '[图片]' + } else if (lastMessage.messageType === 2) { + // 文件消息 + displayContent = `[文件] ${lastMessage.fileName || '未知文件'}` + } + + // 更新联系人的最后消息 + contact.lastMessage = displayContent + contact.lastMessageTime = formatTime(lastMessage.createTime || lastMessage.timestamp) + + console.log('✅ 群聊最后消息:', { + chatId: contact.id, + chatName: contact.name, + lastMessage: displayContent, + lastMessageTime: contact.lastMessageTime + }) + } + } catch (error) { + console.warn('⚠️ 获取群聊最后消息失败:', contact.id, error) + } + }) + + await Promise.all(promises) + console.log('🎉 所有群聊最后消息加载完成') + + } catch (error) { + console.error('❌ 加载所有群聊最后消息失败:', error) + } +} + +// 获取当前用户的免打扰状态 +const loadCurrentUserNotDisturbStatus = async (chatId: string) => { + try { + // 从群聊成员列表中获取当前用户的免打扰状态 + const response = await ChatApi.getChatMembers(chatId) + + if (response.data && response.data.success) { + const members = response.data.result || [] + const currentUserId = userStore.user?.id + + // 查找当前用户在成员列表中的信息 + const currentUserMember = members.find((member: any) => member.id === currentUserId) + + if (currentUserMember && currentUserMember.izNotDisturb !== undefined) { + groupSettings.value.messageMute = currentUserMember.izNotDisturb === 1 + console.log('📱 获取当前用户免打扰状态成功:', { + chatId, + currentUserId, + izNotDisturb: currentUserMember.izNotDisturb, + messageMute: groupSettings.value.messageMute + }) + } else { + // 如果找不到当前用户信息,使用默认值 + groupSettings.value.messageMute = false + console.log('📱 未找到当前用户免打扰状态,使用默认值:', { chatId, currentUserId }) + } + } else { + // 使用默认值 + groupSettings.value.messageMute = false + console.warn('⚠️ 获取群聊成员失败,使用默认值:', response.data?.message) + } + } catch (error) { + console.error('❌ 获取当前用户免打扰状态失败:', error) + // 使用默认值 + groupSettings.value.messageMute = false + } +} + // 获取群聊详情(包括禁言状态) const loadChatDetail = async (chatId: string) => { try { @@ -703,6 +939,9 @@ const loadChatDetail = async (chatId: string) => { } } + // 获取当前用户的免打扰状态 + await loadCurrentUserNotDisturbStatus(chatId) + return chatDetail } } catch (error) { @@ -713,6 +952,9 @@ const loadChatDetail = async (chatId: string) => { if (contact && contact.izAllMuted !== undefined) { isAllMuted.value = contact.izAllMuted === 1 || contact.izAllMuted === true } + + // 仍然尝试获取免打扰状态 + await loadCurrentUserNotDisturbStatus(chatId) } } @@ -801,7 +1043,7 @@ const handleMuteMember = async () => { try { const member = selectedMember.value - const isCurrentlyMuted = member.isMuted || false + const isCurrentlyMuted = member.izMuted === 1 if (isCurrentlyMuted) { // 解除禁言 @@ -810,7 +1052,7 @@ const handleMuteMember = async () => { message.success(`已解除 ${member.realname} 的禁言`) // 更新本地状态 - member.isMuted = false + member.izMuted = 0 } else { // 禁言 console.log('🔒 禁言用户:', { chatId: activeContactId.value, userId: member.id, memberName: member.realname }) @@ -818,7 +1060,7 @@ const handleMuteMember = async () => { message.success(`已禁言 ${member.realname}`) // 更新本地状态 - member.isMuted = true + member.izMuted = 1 } // 关闭弹框 @@ -832,34 +1074,108 @@ const handleMuteMember = async () => { } } -// 处理移除成员 -const handleRemoveMember = async () => { - if (!activeContactId.value || !selectedMember.value) return + +// 处理当前用户对群聊的免打扰设置 +const handleCurrentUserNotDisturb = async () => { + if (!activeContactId.value || !userStore.user?.id) return isMuteLoading.value = true try { - const member = selectedMember.value - // await ChatApi.removeMember(activeContactId.value, member.id) - message.success(`已移除 ${member.realname}`) + const currentUserId = userStore.user.id + const isCurrentlyNotDisturb = groupSettings.value.messageMute - // 从本地列表中移除 - const index = groupMembers.value.findIndex((m: any) => m.id === member.id) - if (index > -1) { - groupMembers.value.splice(index, 1) + if (isCurrentlyNotDisturb) { + // 开启免打扰 + console.log('🔕 开启当前用户免打扰:', { chatId: activeContactId.value, userId: currentUserId }) + await ChatApi.enableNotDisturb(activeContactId.value, currentUserId) + message.success('已开启群聊免打扰') + + // 更新本地状态 + groupSettings.value.messageMute = true + } else { + // 关闭免打扰 + console.log('🔔 关闭当前用户免打扰:', { chatId: activeContactId.value, userId: currentUserId }) + await ChatApi.disableNotDisturb(activeContactId.value, currentUserId) + message.success('已关闭群聊免打扰') + + // 更新本地状态 + groupSettings.value.messageMute = false } - // 关闭弹框 - closeMemberModal() + // 更新联系人列表中的状态 + const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value) + if (contact) { + contact.izNotDisturb = groupSettings.value.messageMute ? 1 : 0 + } } catch (error) { - console.error('❌ 移除成员操作失败:', error) + console.error('❌ 当前用户免打扰操作失败:', error) message.error('操作失败,请重试') + // 回滚状态 + groupSettings.value.messageMute = !groupSettings.value.messageMute } finally { isMuteLoading.value = false } } +// 更新联系人的最后消息 +const updateContactLastMessage = (chatId: string, lastMessage: Message) => { + const contact = contacts.value.find((c: Contact) => c.id === chatId) + if (contact) { + // 根据消息类型生成显示内容 + let displayContent = '' + if (lastMessage.type === 'text') { + displayContent = lastMessage.content + } else if (lastMessage.type === 'image') { + displayContent = '[图片]' + } else if (lastMessage.type === 'file') { + displayContent = `[文件] ${lastMessage.fileName || '未知文件'}` + } + + // 更新联系人的最后消息和时间 + contact.lastMessage = displayContent + contact.lastMessageTime = lastMessage.time + + console.log('📝 更新联系人最后消息:', { + chatId, + contactName: contact.name, + lastMessage: displayContent, + lastMessageTime: lastMessage.time + }) + } +} + +// 更新最后读取消息ID +const updateLastReadMessage = async (messageId: string) => { + if (!activeContactId.value || !messageId) { + console.warn('⚠️ 无法更新最后读取消息ID: 缺少必要参数', { + activeContactId: activeContactId.value, + messageId + }) + return + } + + try { + console.log('📖 更新最后读取消息ID:', { chatId: activeContactId.value, messageId }) + await ChatApi.updateLastRead(activeContactId.value, messageId) + console.log('✅ 最后读取消息ID更新成功') + } catch (error) { + console.error('❌ 更新最后读取消息ID失败:', error) + // 不显示错误提示给用户,因为这是后台操作 + // 但记录详细错误信息用于调试 + if (error instanceof Error) { + console.error('错误详情:', error.message) + } + } +} + +// 处理移除成员 - 暂时注释掉,功能未实现 +// const handleRemoveMember = async () => { +// if (!activeContactId.value || !selectedMember.value) return +// // 功能待实现 +// } + // 加载指定会话的消息 const loadMessages = async (chatId: string) => { messagesLoading.value = true @@ -875,8 +1191,19 @@ const loadMessages = async (chatId: string) => { console.log('📝 消息列表数据:', response.data.result) // 转换API数据为组件需要的格式 - messages.value = response.data.result.map((msg: any): Message => { + // 先按时间正序排序(旧消息在前,新消息在后) + const sortedMessages = response.data.result.sort((a: any, b: any) => { + const timeA = new Date(a.createTime || 0).getTime() + const timeB = new Date(b.createTime || 0).getTime() + return timeA - timeB + }) + // 转换消息并添加时间分组信息 + const processedMessages: Message[] = [] + let lastMessageTime: Date | null = null + let lastSenderId: string | null = null + + sortedMessages.forEach((msg: any) => { // 根据messageType数字判断消息类型:0=文本,1=图片,2=文件 let messageType = 'text' if (msg.messageType === 1) { @@ -885,115 +1212,77 @@ const loadMessages = async (chatId: string) => { messageType = 'file' } - return { + // 判断是否是当前用户发送的消息 + const currentUserId = String(userStore.user?.id || '') + const messageSenderId = String(msg.senderInfo?.id || msg.senderId || '') + const isOwnMessage = currentUserId === messageSenderId + + // 计算时间差,判断是否需要显示时间 + const currentMessageTime = new Date(msg.createTime || new Date().toISOString()) + const timeDiff = lastMessageTime ? currentMessageTime.getTime() - lastMessageTime.getTime() : Infinity + const shouldShowTime = !lastMessageTime || timeDiff > 5 * 60 * 1000 // 5分钟间隔 + + // 判断是否与上一条消息是同一人发送 + const isSameSender = lastSenderId === messageSenderId + + // 对于文件消息,从URL中提取文件名 + let fileName = msg.fileName + let fileUrl = msg.fileUrl + let fileType = msg.fileType || '' + + if (messageType === 'file' && msg.content && !fileName) { + // 从URL中提取文件名 + const urlParts = msg.content.split('/') + fileName = urlParts[urlParts.length - 1] || '未知文件' + fileUrl = msg.content + } + if (messageType === 'image' && msg.content && !fileUrl) { + fileUrl = msg.content + } + + // 从文件名中提取文件类型 + if (messageType === 'file' && fileName && !fileType) { + const extension = fileName.split('.').pop()?.toLowerCase() || '' + fileType = extension + } + + const message: Message = { id: msg.id, contactId: msg.chatId, type: messageType as 'text' | 'image' | 'file', content: msg.content, senderName: msg.senderInfo?.realname || '未知用户', - senderId: msg.senderInfo?.id || msg.senderId || '', // 发送者ID + senderId: messageSenderId, avatar: msg.senderInfo?.avatar || '', - time: formatTime(msg.createTime), - isOwn: false, // TODO: 需要根据当前用户ID判断 - isRead: msg.isRead || false, // 消息是否已读 - fileName: msg.fileName, + time: formatTime(msg.createTime || new Date().toISOString()), + isOwn: isOwnMessage, + isRead: msg.isRead || false, + fileName: fileName, fileSize: msg.fileSize, - fileUrl: msg.fileUrl + fileUrl: fileUrl, + fileType: fileType, + // 添加时间分组相关字段 + showTime: shouldShowTime, + showSender: !isSameSender || shouldShowTime // 不同发送者或需要显示时间时显示发送者 } + + processedMessages.push(message) + + // 更新状态 + lastMessageTime = currentMessageTime + lastSenderId = messageSenderId }) - // 添加多条模拟消息用于测试滚动效果 - 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 = processedMessages - // 添加模拟消息到消息列表 - messages.value.push(...mockMessages) + // 消息加载完成后,更新最后读取消息ID + if (messages.value.length > 0) { + const lastMessage = messages.value[messages.value.length - 1] + await updateLastReadMessage(lastMessage.id) - console.log('✅ 最终消息列表:', messages.value) + // 更新联系人的最后消息 + updateContactLastMessage(chatId, lastMessage) + } } else { console.warn('⚠️ API返回失败:', response.data) @@ -1004,17 +1293,28 @@ const loadMessages = async (chatId: string) => { messages.value = [] } finally { messagesLoading.value = false - // 加载完消息后滚动到底部 - nextTick(() => { + // 加载完消息后滚动到底部 - 使用setTimeout确保DOM更新完成 + setTimeout(() => { scrollToBottom() - }) + }, 100) } } // 滚动到底部 const scrollToBottom = () => { if (messagesContainer.value) { - messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight + // 使用平滑滚动 + messagesContainer.value.scrollTo({ + top: messagesContainer.value.scrollHeight, + behavior: 'smooth' + }) + + // 备用方案:直接设置scrollTop + setTimeout(() => { + if (messagesContainer.value) { + messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight + } + }, 50) } } @@ -1124,35 +1424,50 @@ const handleSendMessage = async (content: string) => { messages.value.push(newMessage) // 更新联系人最后消息 - const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value) - if (contact) { - contact.lastMessage = content - contact.lastMessageTime = newMessage.time - } + updateContactLastMessage(activeContactId.value, newMessage) // 发送消息后滚动到底部 nextTick(() => { scrollToBottom() }) - // 模拟发送到服务器 + // 发送到服务器 try { - // 模拟网络延迟 - await new Promise(resolve => setTimeout(resolve, 300)) + console.log('📤 发送文本消息:', { + chat_id: activeContactId.value, + content, + messageType: 0 + }) - // 模拟成功响应 + const response = await ChatApi.sendMessage({ + chat_id: activeContactId.value, + content, + messageType: 0 // 0=文本消息 + }) - // 模拟对方回复(随机回复) - setTimeout(() => { - if (activeContactId.value) { - simulateReply(activeContactId.value) + if (response.data && response.data.success) { + console.log('✅ 文本消息发送成功:', response.data) + + // 更新消息ID为服务器返回的ID + // 服务器返回的ID在 result 字段中,不是 result.id + if (response.data.result) { + newMessage.id = response.data.result + console.log('🔄 更新消息ID:', newMessage.id) + + // 使用服务器返回的真实ID更新最后读取消息ID + await updateLastReadMessage(newMessage.id) + } else { + console.warn('⚠️ 服务器未返回消息ID,跳过更新最后读取消息') } - }, 1000 + Math.random() * 2000) // 1-3秒后回复 + } else { + throw new Error(response.data?.message || '发送失败') + } } catch (error) { - console.error('发送消息失败:', error) + console.error('❌ 发送文本消息失败:', error) message.error('发送消息失败') - // 如果发送失败,可以从本地消息列表中移除 + + // 如果发送失败,从本地消息列表中移除 const index = messages.value.findIndex((msg: Message) => msg.id === newMessage.id) if (index > -1) { messages.value.splice(index, 1) @@ -1164,51 +1479,6 @@ const handleSendMessage = async (content: string) => { }) } -// 模拟对方回复 -const simulateReply = (chatId: string) => { - const replies = [ - '好的,我明白了', - '谢谢你的消息', - '收到!', - '没问题', - '好的,我会处理的', - '明白了', - '收到收到', - '好的,稍等', - '我看到了', - '嗯嗯,知道了' - ] - - const randomReply = replies[Math.floor(Math.random() * replies.length)] - - const replyMessage: Message = { - id: (Date.now() + Math.random()).toString(), - contactId: chatId, - 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, - isRead: true - } - - messages.value.push(replyMessage) - - // 更新联系人最后消息 - const contact = contacts.value.find((c: Contact) => c.id === chatId) - if (contact) { - contact.lastMessage = randomReply - contact.lastMessageTime = replyMessage.time - contact.unreadCount = (contact.unreadCount || 0) + 1 - } - - // 收到回复后滚动到底部 - nextTick(() => { - scrollToBottom() - }) -} const previewImage = (_src: string) => { // TODO: 实现图片预览功能 @@ -1216,27 +1486,63 @@ const previewImage = (_src: string) => { // 获取文件图标 const getFileIcon = (type: string) => { - if (type.includes('pdf')) return '/images/profile/pdf.png' - if (type.includes('word') || type.includes('document')) return '/images/profile/word.png' - if (type.includes('excel') || type.includes('spreadsheet')) return '/images/profile/xls.png' - if (type.includes('powerpoint') || type.includes('presentation')) return '/images/profile/ppt.png' - if (type.includes('zip') || type.includes('rar')) return '/images/profile/zip.png' - if (type.includes('text')) return '/images/profile/doc.png' + if (!type) return '/images/profile/file.png' + + const lowerType = type.toLowerCase() + + // 文档类型 + if (lowerType === 'pdf') return '/images/profile/pdf.png' + if (lowerType === 'doc' || lowerType === 'docx') return '/images/profile/word.png' + if (lowerType === 'xls' || lowerType === 'xlsx') return '/images/profile/xls.png' + if (lowerType === 'ppt' || lowerType === 'pptx') return '/images/profile/ppt.png' + if (lowerType === 'txt' || lowerType === 'text') return '/images/profile/doc.png' + + // 压缩文件 + if (lowerType === 'zip' || lowerType === 'rar' || lowerType === '7z' || lowerType === 'tar' || lowerType === 'gz') { + return '/images/profile/zip.png' + } + + // 图片文件 + if (lowerType === 'jpg' || lowerType === 'jpeg' || lowerType === 'png' || lowerType === 'gif' || lowerType === 'bmp' || lowerType === 'webp') { + return '/images/profile/image.png' + } + + // 视频文件 + if (lowerType === 'mp4' || lowerType === 'avi' || lowerType === 'mov' || lowerType === 'wmv' || lowerType === 'flv') { + return '/images/profile/video.png' + } + + // 音频文件 + if (lowerType === 'mp3' || lowerType === 'wav' || lowerType === 'flac' || lowerType === 'aac') { + return '/images/profile/audio.png' + } + + // 代码文件 + if (lowerType === 'js' || lowerType === 'ts' || lowerType === 'html' || lowerType === 'css' || lowerType === 'json' || lowerType === 'xml') { + return '/images/profile/code.png' + } + return '/images/profile/file.png' } // 下载文件 const downloadFile = (message: any) => { - // 这里可以实现文件下载逻辑 - // 可以创建一个临时的下载链接 - if (message.fileUrl) { + // 使用 fileUrl 或 content 作为下载链接 + const downloadUrl = message.fileUrl || message.content + const fileName = message.fileName || '下载文件' + + if (downloadUrl) { + console.log('📥 下载文件:', { fileName, downloadUrl }) const link = document.createElement('a') - link.href = message.fileUrl - link.download = message.fileName + link.href = downloadUrl + link.download = fileName + link.target = '_blank' // 在新标签页打开 + document.body.appendChild(link) link.click() + document.body.removeChild(link) } else { - // 如果没有文件URL,可以提示用户 - alert('文件下载功能待实现') + console.error('❌ 无法下载文件,缺少文件URL') + message.error('文件下载失败,缺少文件链接') } } @@ -1255,153 +1561,249 @@ const handleEmoji = () => { // TODO: 实现表情选择功能 } -const handleImage = (imageData: any) => { +const handleImage = async (imageData: any) => { // 验证参数 - if (!imageData || !imageData.url) { - console.error('图片数据无效:', imageData) + if (!imageData || !imageData.file) { + console.error('图片文件无效:', imageData) return } + if (!activeContactId.value) return + + // 先上传文件 try { - // 创建图片消息 + console.log('📤 上传图片文件:', imageData.file.name) + const uploadResponse = await ChatApi.uploadFile(imageData.file) + + console.log('🔍 上传接口返回数据:', uploadResponse.data) + + if (!uploadResponse.data || !uploadResponse.data.success) { + throw new Error(uploadResponse.data?.message || '文件上传失败') + } + + // 尝试多种方式获取文件URL + const fileUrl = uploadResponse.data.message || // 文件URL在message字段中 + uploadResponse.data.result?.url || + uploadResponse.data.result || + uploadResponse.data.data?.url || + uploadResponse.data.data + + // 确保fileUrl是字符串类型 + const finalFileUrl = typeof fileUrl === 'string' ? fileUrl : (fileUrl?.url || '') + console.log('✅ 图片上传成功,提取的URL:', finalFileUrl) + const imageMessage: Message = { id: Date.now().toString(), - contactId: activeContactId.value!, + contactId: activeContactId.value, type: 'image', - content: imageData.url, - fileName: imageData.name || '图片', - fileSize: imageData.size?.toString() || '0', + content: finalFileUrl, // 使用上传后的URL + fileName: imageData.file.name || '图片', + fileSize: imageData.file.size?.toString() || '0', + fileType: imageData.file.type || 'image/jpeg', senderName: userStore.user?.profile?.realName || userStore.user?.nickname || '我', - senderId: userStore.user?.id?.toString() || '', // 发送者ID + senderId: userStore.user?.id?.toString() || '', avatar: userStore.user?.avatar || '', time: formatTime(new Date().toISOString()), isOwn: true, isRead: false } - // 添加到当前聊天 - if (activeContactId.value) { - // 添加到消息列表 - messages.value.push(imageMessage) + // 先添加到本地消息列表 + messages.value.push(imageMessage) - // 更新联系人最后消息 - const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value) - if (contact) { - contact.lastMessage = '[图片]' - contact.lastMessageTime = formatTime(new Date().toISOString()) - } + // 更新联系人最后消息 + updateContactLastMessage(activeContactId.value, imageMessage) + + // 发送消息后滚动到底部 + nextTick(() => { + scrollToBottom() + }) + + // 发送消息到服务器 + console.log('📤 发送图片消息:', { + chat_id: activeContactId.value, + content: finalFileUrl, + messageType: 1, + fileUrl: finalFileUrl, + fileSize: imageData.file.size, + fileType: imageData.file.type, + fileName: imageData.file.name + }) + + const sendData = { + chat_id: activeContactId.value, + content: finalFileUrl, // 上传后的URL作为内容 + messageType: 1, // 1=图片消息 + fileUrl: finalFileUrl, + fileSize: imageData.file.size, + fileType: imageData.file.type, + fileName: imageData.file.name } - // 模拟发送图片到服务器 - setTimeout(() => { - // 消息已发送 + console.log('📤 发送图片消息数据:', sendData) + console.log('📤 发送图片消息数据类型检查:', { + messageType: sendData.messageType, + messageTypeType: typeof sendData.messageType, + content: sendData.content, + fileUrl: sendData.fileUrl + }) + const response = await ChatApi.sendMessage(sendData) + console.log('📤 发送图片消息响应:', response.data) - // 模拟对方回复 - setTimeout(() => { - const replyMessage: Message = { - id: (Date.now() + 1).toString(), - contactId: activeContactId.value!, - type: 'text', - content: '收到图片了!', - senderName: '对方', - senderId: '2000000000', // 对方用户ID - avatar: 'https://avatars.githubusercontent.com/u/38358644', - time: formatTime(new Date().toISOString()), - isOwn: false, - isRead: false - } + if (response.data && response.data.success) { + console.log('✅ 图片消息发送成功:', response.data) - if (activeContactId.value) { - // 添加到消息列表 - messages.value.push(replyMessage) + // 更新消息ID为服务器返回的ID + if (response.data.result) { + imageMessage.id = response.data.result + console.log('🔄 更新图片消息ID:', imageMessage.id) + + // 使用服务器返回的真实ID更新最后读取消息ID + await updateLastReadMessage(imageMessage.id) + + // 重新加载消息列表以确认服务器存储的数据 + console.log('🔄 重新加载消息列表以确认服务器数据...') + await loadMessages(activeContactId.value) + } else { + console.warn('⚠️ 服务器未返回图片消息ID,跳过更新最后读取消息') + } + } else { + throw new Error(response.data?.message || '发送失败') + } - // 更新联系人最后消息 - const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value) - if (contact) { - contact.lastMessage = '收到图片了!' - contact.lastMessageTime = formatTime(new Date().toISOString()) - } - } - }, 1000) - }, 500) } catch (error) { - console.error('处理图片消息时出错:', error) + console.error('❌ 处理图片失败:', error) + message.error('发送图片失败') } + + nextTick(() => { + scrollToBottom() + }) } -const handleFile = (fileData: any) => { +const handleFile = async (fileData: any) => { // 验证参数 if (!fileData || !fileData.file) { console.error('文件数据无效:', fileData) return } + if (!activeContactId.value) return + + // 先上传文件 try { - // 创建文件消息 + console.log('📤 上传文件:', fileData.file.name) + const uploadResponse = await ChatApi.uploadFile(fileData.file) + + console.log('🔍 上传接口返回数据:', uploadResponse.data) + + if (!uploadResponse.data || !uploadResponse.data.success) { + throw new Error(uploadResponse.data?.message || '文件上传失败') + } + + // 尝试多种方式获取文件URL + const fileUrl = uploadResponse.data.message || // 文件URL在message字段中 + uploadResponse.data.result?.url || + uploadResponse.data.result || + uploadResponse.data.data?.url || + uploadResponse.data.data + + // 确保fileUrl是字符串类型 + const finalFileUrl = typeof fileUrl === 'string' ? fileUrl : (fileUrl?.url || '') + console.log('✅ 文件上传成功,提取的URL:', finalFileUrl) + + // 从文件名中提取文件扩展名 + const fileName = fileData.file.name + const fileExtension = fileName.split('.').pop()?.toLowerCase() || '' + const fileMessage: Message = { id: Date.now().toString(), - contactId: activeContactId.value!, + contactId: activeContactId.value, type: 'file', - content: fileData.name, - fileName: fileData.name, - fileSize: fileData.size.toString(), - fileType: fileData.type, + content: finalFileUrl, // 使用上传后的URL + fileName: fileName, + fileSize: fileData.file.size.toString(), + fileType: fileExtension, // 使用文件扩展名而不是MIME类型 + fileUrl: finalFileUrl, senderName: userStore.user?.profile?.realName || userStore.user?.nickname || '我', - senderId: userStore.user?.id?.toString() || '', // 发送者ID + senderId: userStore.user?.id?.toString() || '', avatar: userStore.user?.avatar || '', time: formatTime(new Date().toISOString()), isOwn: true, isRead: false } - // 添加到当前聊天 - if (activeContactId.value) { - // 添加到消息列表 - messages.value.push(fileMessage) + // 先添加到本地消息列表 + messages.value.push(fileMessage) - // 更新联系人最后消息 - const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value) - if (contact) { - contact.lastMessage = '[文件]' - contact.lastMessageTime = formatTime(new Date().toISOString()) - } + // 更新联系人最后消息 + updateContactLastMessage(activeContactId.value, fileMessage) + + // 发送消息后滚动到底部 + nextTick(() => { + scrollToBottom() + }) + + // 发送消息到服务器 + console.log('📤 发送文件消息:', { + chat_id: activeContactId.value, + content: finalFileUrl, + messageType: 2, + fileUrl: finalFileUrl, + fileSize: fileData.file.size, + fileType: fileData.file.type, + fileName: fileData.file.name + }) + + const sendData = { + chat_id: activeContactId.value, + content: finalFileUrl, // 上传后的URL作为内容 + messageType: 2, // 2=文件消息 + fileUrl: finalFileUrl, + fileSize: fileData.file.size, + fileType: fileData.file.type, + fileName: fileData.file.name } - // 模拟发送文件到服务器 - setTimeout(() => { - // 消息已发送 + console.log('📤 发送文件消息数据:', sendData) + console.log('📤 发送文件消息参数检查:', { + fileName: sendData.fileName, + fileUrl: sendData.fileUrl, + fileSize: sendData.fileSize, + fileType: sendData.fileType + }) + const response = await ChatApi.sendMessage(sendData) + console.log('📤 发送文件消息响应:', response.data) - // 模拟对方回复 - setTimeout(() => { - const replyMessage: Message = { - id: (Date.now() + 1).toString(), - contactId: activeContactId.value!, - type: 'text', - content: '收到文件了!', - senderName: '对方', - senderId: '2000000000', // 对方用户ID - avatar: 'https://avatars.githubusercontent.com/u/38358644', - time: formatTime(new Date().toISOString()), - isOwn: false, - isRead: false - } + if (response.data && response.data.success) { + console.log('✅ 文件消息发送成功:', response.data) - if (activeContactId.value) { - // 添加到消息列表 - messages.value.push(replyMessage) + // 更新消息ID为服务器返回的ID + if (response.data.result) { + fileMessage.id = response.data.result + console.log('🔄 更新文件消息ID:', fileMessage.id) + + // 使用服务器返回的真实ID更新最后读取消息ID + await updateLastReadMessage(fileMessage.id) + + // 重新加载消息列表以确认服务器存储的数据 + console.log('🔄 重新加载消息列表以确认服务器数据...') + await loadMessages(activeContactId.value) + } else { + console.warn('⚠️ 服务器未返回文件消息ID,跳过更新最后读取消息') + } + } else { + throw new Error(response.data?.message || '发送失败') + } - // 更新联系人最后消息 - const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value) - if (contact) { - contact.lastMessage = '收到文件了!' - contact.lastMessageTime = formatTime(new Date().toISOString()) - } - } - }, 1000) - }, 500) } catch (error) { - console.error('处理文件消息时出错:', error) + console.error('❌ 处理文件失败:', error) + message.error('发送文件失败') } + + nextTick(() => { + scrollToBottom() + }) } // 生命周期钩子 @@ -1416,7 +1818,7 @@ onMounted(() => {