From ce54a41f4a1990e664e548bf7c73ed699c8a2667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=BC=A0?= <2091066548@qq.com> Date: Tue, 23 Sep 2025 16:23:02 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E7=BB=83=E4=B9=A0=E8=AE=A8?= =?UTF-8?q?=E8=AE=BA=E5=AF=B9=E6=8E=A5=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/modules/ai.ts | 16 +- src/api/modules/course.ts | 34 +++ .../ExamComponents/QuestionBankModal.vue | 2 +- src/components/course/DPlayerVideo.vue | 73 +++++ src/views/CourseExchanged.vue | 264 ++++++++++++------ src/views/ExamNotice.vue | 6 +- 6 files changed, 299 insertions(+), 96 deletions(-) diff --git a/src/api/modules/ai.ts b/src/api/modules/ai.ts index 891541f..799b4e3 100644 --- a/src/api/modules/ai.ts +++ b/src/api/modules/ai.ts @@ -176,6 +176,7 @@ export class AIApi { const decoder = new TextDecoder() let buffer = '' + // @ts-ignore let messageEndReceived = false let endTimeout: NodeJS.Timeout | null = null @@ -330,20 +331,21 @@ export class AIApi { * @returns Promise */ static async sendChatMessageWithEventSource( - content: string, + // @ts-ignore + content: string, // TODO: 需要在实际实现中使用此参数 onMessage: (chunk: string) => void, onComplete?: () => void, onError?: (error: any) => void ): Promise { - const request: AIChatRequest = { - appId: "1970031066993217537", - content: content, - responseMode: "streaming" - } + // const request: AIChatRequest = { + // appId: "1970031066993217537", + // content: content, + // responseMode: "streaming" + // } // 暂时未使用 try { // 获取token - const token = localStorage.getItem('token') + // const token = localStorage.getItem('token') // 暂时未使用 // 构建URL参数 const url = new URL(`${import.meta.env.VITE_API_BASE_URL}/airag/chat/send`) diff --git a/src/api/modules/course.ts b/src/api/modules/course.ts index 085d7d5..e03ef60 100644 --- a/src/api/modules/course.ts +++ b/src/api/modules/course.ts @@ -916,6 +916,40 @@ export class CourseApi { } } + // 获取考试题目列表 + static async getExamQuestions(examId: string, studentId: string): Promise> { + try { + console.log('🚀 调用获取考试题目API,考试ID:', examId, '学生ID:', studentId) + + // 直接在URL中添加studentId参数 + const url = `/aiol/aiolExam/getExamQuestions/${examId}?studentId=${studentId}` + console.log('🔍 请求URL:', url) + + const response = await ApiRequest.get(url) + console.log('🔍 考试题目API响应:', response) + + return response + } catch (error) { + console.error('❌ 获取考试题目失败:', error) + throw error + } + } + + // 获取题目详情 + static async getQuestionDetail(questionId: string): Promise> { + try { + console.log('🚀 调用获取题目详情API,题目ID:', questionId) + + const response = await ApiRequest.get(`/aiol/aiolRepo/repoList/${questionId}`) + console.log('🔍 题目详情API响应:', response) + + return response + } catch (error) { + console.error('❌ 获取题目详情失败:', error) + throw error + } + } + // 获取章节讨论 static async getSectionDiscussion(courseId: string, sectionId: string): Promise> { try { diff --git a/src/components/admin/ExamComponents/QuestionBankModal.vue b/src/components/admin/ExamComponents/QuestionBankModal.vue index e42fba1..6d7e794 100644 --- a/src/components/admin/ExamComponents/QuestionBankModal.vue +++ b/src/components/admin/ExamComponents/QuestionBankModal.vue @@ -220,7 +220,7 @@ const categoryList = ref([]); const repoOptions = computed(() => [ { label: '全部题库', value: '' }, ...repoList.value.map((repo: Repo) => ({ - label: `${repo.title} (${repo.questionCount || 0}题)`, + label: `${repo.title} (${repo.question_count || 0}题)`, value: repo.id })) ]); diff --git a/src/components/course/DPlayerVideo.vue b/src/components/course/DPlayerVideo.vue index e228149..13ca601 100644 --- a/src/components/course/DPlayerVideo.vue +++ b/src/components/course/DPlayerVideo.vue @@ -664,6 +664,9 @@ const createManualQualityButton = () => { option.addEventListener('mouseleave', () => { if (quality.value !== props.currentQuality) { option.style.backgroundColor = 'transparent' + } else { + // 如果是当前选中的清晰度,保持蓝色背景 + option.style.backgroundColor = '#007bff' } }) @@ -698,6 +701,11 @@ const createManualQualityButton = () => { // 标记按钮已创建 qualityButtonCreated.value = true + // 初始化菜单选项样式 + setTimeout(() => { + updateQualityMenuStyles(props.currentQuality) + }, 100) + console.log('✅ 手动清晰度按钮创建完成') } @@ -707,6 +715,68 @@ const getCurrentQualityLabel = () => { return current ? current.label : '360p' } +// 更新清晰度菜单选项的样式 +const updateQualityMenuStyles = (currentQualityValue: string) => { + const qualityMenu = dplayerContainer.value?.querySelector('.manual-quality .dplayer-quality-list') + if (qualityMenu) { + const qualityItems = qualityMenu.querySelectorAll('.dplayer-quality-item') + qualityItems.forEach((item, index) => { + const quality = props.videoQualities[index] + if (quality) { + const isActive = quality.value === currentQualityValue + const itemElement = item as HTMLElement + + // 设置基础样式 + itemElement.style.cssText = ` + padding: 8px 12px; + color: #fff; + font-size: 13px; + cursor: pointer; + transition: background-color 0.2s; + text-align: center; + ${isActive ? 'background: #007bff;' : 'background: transparent;'} + ` + + // 重新绑定鼠标事件,确保当前选中项的样式正确 + const newItemElement = itemElement.cloneNode(true) as HTMLElement + itemElement.parentNode?.replaceChild(newItemElement, itemElement) + + // 重新绑定事件 + newItemElement.addEventListener('mouseenter', () => { + if (quality.value !== currentQualityValue) { + newItemElement.style.backgroundColor = 'rgba(255, 255, 255, 0.1)' + } + }) + + newItemElement.addEventListener('mouseleave', () => { + if (quality.value !== currentQualityValue) { + newItemElement.style.backgroundColor = 'transparent' + } else { + // 如果是当前选中的清晰度,保持蓝色背景 + newItemElement.style.backgroundColor = '#007bff' + } + }) + + newItemElement.addEventListener('click', () => { + console.log('🔄 手动切换清晰度到:', quality.label) + switchQuality(quality) + ;(qualityMenu as HTMLElement).style.display = 'none' + + // 更新按钮文字 + const qualityButton = dplayerContainer.value?.querySelector('.manual-quality .dplayer-quality-button') + if (qualityButton) { + qualityButton.textContent = quality.label + } + + // 不需要在这里再次调用updateQualityMenuStyles,因为switchQuality会处理 + }) + + console.log(`🎨 更新清晰度选项样式: ${quality.label} - ${isActive ? '激活' : '未激活'}`) + } + }) + } +} + const switchQuality = (quality: any) => { console.log('🔄 开始切换清晰度:', { quality: quality, @@ -814,6 +884,9 @@ const switchQuality = (quality: any) => { existingButton.textContent = quality.label console.log('✅ 清晰度按钮文字已立即更新为:', quality.label) } + + // 更新清晰度菜单选项的样式 + updateQualityMenuStyles(quality.value) } diff --git a/src/views/CourseExchanged.vue b/src/views/CourseExchanged.vue index 2ebc521..e794533 100644 --- a/src/views/CourseExchanged.vue +++ b/src/views/CourseExchanged.vue @@ -345,13 +345,13 @@
-

讨论

+

{{ discussionTitle }}

未参与
- 如何理解学风与科研诚信?如何理解优良学风与科研诚信之间的关系?各位同学大家好,欢迎加入本学期的课程学习,在学习过程有任何问题困惑和不同见解,都欢迎同学在讨论区积极交流,踊跃回复老师留在讨论区的问题。 + {{ discussionDescription }}
@@ -1347,6 +1347,7 @@ import { useMessage } from 'naive-ui' import { CourseApi } from '@/api/modules/course' import { CommentApi } from '@/api/modules/comment' import { AIApi } from '@/api/modules/ai' +import { AuthApi } from '@/api/modules/auth' import type { Course, CourseSection, CourseComment } from '@/api/types' import QuillEditor from '@/components/common/QuillEditor.vue' import DPlayerVideo from '@/components/course/DPlayerVideo.vue' @@ -1580,6 +1581,8 @@ const practiceFinished = ref(false) const discussionMode = ref(false) const currentDiscussionSection = ref(null) const discussionList = ref([]) +const discussionTitle = ref('讨论') +const discussionDescription = ref('') const newComment = ref('') const replyingTo = ref(null) @@ -2124,71 +2127,13 @@ const loadCourseSections = async () => { console.log('✅ API返回的原始章节数据:', response.data.list) console.log('✅ 章节数据数量:', response.data.list.length) - // 添加模拟练习章节 - const sectionsWithPractice = [...response.data.list] - - // 添加一个练习章节到第一章 - const practiceSection: CourseSection = { - id: '999999', // 使用一个特殊的ID - lessonId: '999999', - name: 'JavaScript基础练习', - type: 5, // 练习类型 - level: 2, // 二级章节 - parentId: sectionsWithPractice.find(s => s.level === 1)?.id || '1', // 找到第一个父章节 - duration: '30分钟', - completed: false, - outline: '', - sort: 999, - revision: 1, - createdAt: Date.now(), - updatedAt: Date.now(), - deletedAt: null - } - - // 将练习章节插入到合适的位置(第一章的最后) - const firstChapterSections = sectionsWithPractice.filter(s => s.level === 2 && s.parentId === practiceSection.parentId) - if (firstChapterSections.length > 0) { - // 插入到第一章的最后一个章节后面 - const insertIndex = sectionsWithPractice.findIndex(s => s.id === firstChapterSections[firstChapterSections.length - 1].id) + 1 - sectionsWithPractice.splice(insertIndex, 0, practiceSection) - } else { - // 如果没有找到合适位置,就添加到最后 - sectionsWithPractice.push(practiceSection) - } - - // 添加一个讨论章节 - const discussionSection: CourseSection = { - id: '999998', // 使用另一个特殊的ID - lessonId: '999998', - name: '机器学习与科研流程讨论', - type: 6, // 讨论类型(自定义) - level: 2, // 二级章节 - parentId: sectionsWithPractice.find(s => s.level === 1)?.id || '1', // 找到第一个父章节 - duration: '讨论', - completed: false, - outline: '', - sort: 998, - revision: 1, - createdAt: Date.now(), - updatedAt: Date.now(), - deletedAt: null - } - - // 将讨论章节插入到练习章节后面 - const practiceIndex = sectionsWithPractice.findIndex(s => s.id === practiceSection.id) - if (practiceIndex !== -1) { - sectionsWithPractice.splice(practiceIndex + 1, 0, discussionSection) - } else { - sectionsWithPractice.push(discussionSection) - } - - courseSections.value = sectionsWithPractice - groupedSections.value = groupSectionsByChapter(sectionsWithPractice) + // 直接使用API返回的章节数据 + courseSections.value = response.data.list + groupedSections.value = groupSectionsByChapter(response.data.list) console.log('✅ 设置后的courseSections:', courseSections.value) console.log('✅ 设置后的groupedSections:', groupedSections.value) console.log('✅ groupedSections长度:', groupedSections.value.length) - console.log('✅ 已添加练习章节:', practiceSection) } else { console.log('❌ API返回的章节数据为空或格式错误') console.log('❌ response.data:', response.data) @@ -2499,7 +2444,20 @@ const handlePractice = async (section: CourseSection) => { } try { - // 调用章节练习API + // 第一步:获取用户信息 + console.log('👤 获取用户信息...') + const userInfoResponse = await AuthApi.getUserInfo() + + if (!userInfoResponse.success || !userInfoResponse.result?.baseInfo?.id) { + console.error('❌ 获取用户信息失败:', userInfoResponse) + message.error('获取用户信息失败,请重新登录') + return + } + + const studentId = userInfoResponse.result.baseInfo.id + console.log('✅ 获取用户信息成功,学生ID:', studentId) + + // 第二步:调用章节练习API获取考试信息 const response = await CourseApi.getSectionExercise(courseId.value, section.id.toString()) if (response.data && (response.data.code === 200 || response.data.code === 0)) { @@ -2507,21 +2465,120 @@ const handlePractice = async (section: CourseSection) => { // 处理练习数据 const exerciseData = response.data.result - if (exerciseData && Array.isArray(exerciseData)) { - // 设置练习数据 - practiceQuestions.value = exerciseData - currentPracticeSection.value = section - practiceMode.value = true - practiceStarted.value = true // 直接开始练习,不需要点击开始按钮 - practiceFinished.value = false - currentQuestionIndex.value = 0 + if (exerciseData && Array.isArray(exerciseData) && exerciseData.length > 0) { + // 获取第一个考试的ID + const firstExam = exerciseData[0] + const examId = firstExam.id - // 初始化答案数组 - practiceAnswers.value = new Array(exerciseData.length).fill(null).map(() => []) - fillAnswers.value = new Array(exerciseData.length).fill(null).map(() => []) - essayAnswers.value = new Array(exerciseData.length).fill('') + console.log('🔍 获取到考试信息:', firstExam) + console.log('📋 开始获取考试题目,考试ID:', examId, '学生ID:', studentId) - console.log('✅ 练习模式已启动,题目数量:', practiceQuestions.value.length) + // 第三步:根据考试ID和学生ID获取题目列表 + const questionsResponse = await CourseApi.getExamQuestions(examId, studentId) + + if (questionsResponse.data && (questionsResponse.data.code === 200 || questionsResponse.data.code === 0)) { + console.log('✅ 获取考试题目成功:', questionsResponse.data) + + const questionsList = questionsResponse.data.result + if (questionsList && Array.isArray(questionsList) && questionsList.length > 0) { + console.log('📝 题目列表:', questionsList) + + // 第四步:根据每个题目ID获取详细信息 + const detailedQuestions = [] + + for (const questionItem of questionsList) { + try { + console.log('🔍 获取题目详情,题目ID:', questionItem.id) + const detailResponse = await CourseApi.getQuestionDetail(questionItem.id) + + if (detailResponse.data && (detailResponse.data.code === 200 || detailResponse.data.code === 0)) { + const questionDetail = detailResponse.data.result + console.log('✅ 获取题目详情成功:', questionDetail) + detailedQuestions.push(questionDetail) + } else { + console.warn('⚠️ 获取题目详情失败:', questionItem.id, detailResponse.data?.message) + } + } catch (detailError) { + console.error('❌ 获取题目详情异常:', questionItem.id, detailError) + } + } + + if (detailedQuestions.length > 0) { + // 处理题目数据格式,确保符合前端显示要求 + const processedQuestions = detailedQuestions.map((questionData, index) => { + console.log(`🔍 处理题目 ${index + 1}:`, questionData) + + // 解析API返回的数据结构 + const question = questionData.question + const answers = questionData.answer || [] + + // 题目类型映射:0=单选题,1=多选题,2=判断题,3=填空题,4=简答题 + const typeMap: { [key: number]: string } = { + 0: '单选题', + 1: '多选题', + 2: '判断题', + 3: '填空题', + 4: '简答题' + } + + // 提取选项内容 + const options = answers + .sort((a: any, b: any) => a.orderNo - b.orderNo) // 按orderNo排序 + .map((answer: any) => answer.content) + + // 找出正确答案的索引 + const correctAnswers = answers + .filter((answer: any) => answer.izCorrent === 1) + .map((answer: any) => answer.orderNo - 1) // orderNo从1开始,转换为从0开始的索引 + + console.log(`📝 题目选项:`, options) + console.log(`✅ 正确答案索引:`, correctAnswers) + + // 根据API返回的数据结构适配前端需要的格式 + const processedQuestion = { + id: question.id, + title: question.content || `题目 ${index + 1}`, + type: typeMap[question.type] || '单选题', + score: question.score || 5, + options: options, + correctAnswer: correctAnswers.length === 1 ? correctAnswers[0] : correctAnswers, + analysis: question.analysis || '', + difficulty: question.difficulty || 0, + // 保留原始数据以备后用 + originalData: questionData + } + + console.log(`✅ 处理后的题目 ${index + 1}:`, processedQuestion) + return processedQuestion + }) + + // 设置练习数据 + practiceQuestions.value = processedQuestions + currentPracticeSection.value = section + practiceMode.value = true + practiceStarted.value = true // 直接开始练习,不需要点击开始按钮 + practiceFinished.value = false + currentQuestionIndex.value = 0 + + // 初始化答案数组 + practiceAnswers.value = new Array(processedQuestions.length).fill(null).map(() => []) + fillAnswers.value = new Array(processedQuestions.length).fill(null).map(() => []) + essayAnswers.value = new Array(processedQuestions.length).fill('') + + console.log('✅ 练习模式已启动,题目数量:', practiceQuestions.value.length) + console.log('✅ 处理后的题目列表:', practiceQuestions.value) + } else { + console.warn('⚠️ 没有获取到有效的题目详情') + message.warning('没有获取到有效的题目详情') + } + } else { + console.warn('⚠️ 考试题目列表为空:', questionsList) + message.warning('考试题目列表为空') + } + } else { + console.error('❌ 获取考试题目失败:', questionsResponse.data?.message || questionsResponse.message) + message.error(questionsResponse.data?.message || questionsResponse.message || '获取考试题目失败') + } } else { console.warn('⚠️ 练习数据格式异常:', exerciseData) message.warning('练习数据格式异常') @@ -2791,17 +2848,40 @@ const loadDiscussionData = async (section: CourseSection) => { // 处理讨论数据 const discussionData = response.data.result - if (discussionData && Array.isArray(discussionData)) { - // 为每个评论添加点赞状态字段 - discussionList.value = discussionData.map(comment => ({ - ...comment, - isLiked: false, // 默认未点赞 - likes: comment.likes || 0 // 确保有点赞数字段 - })) - console.log('✅ 讨论数据加载完成,讨论数量:', discussionList.value.length) + if (discussionData && Array.isArray(discussionData) && discussionData.length > 0) { + // 获取第一个讨论项的标题和描述 + const firstDiscussion = discussionData[0] + console.log('🔍 讨论数据详情:', firstDiscussion) + + if (firstDiscussion.title) { + discussionTitle.value = firstDiscussion.title + console.log('✅ 设置讨论标题:', firstDiscussion.title) + } + + if (firstDiscussion.description) { + // 去掉HTML标签,但保留文本内容 + const originalDescription = firstDiscussion.description + const strippedDescription = stripHtmlTags(originalDescription) + discussionDescription.value = strippedDescription + console.log('✅ 原始描述:', originalDescription) + console.log('✅ 处理后描述:', strippedDescription) + } + + // 这个API返回的是讨论主题信息,不是评论列表 + // 评论列表可能需要另外的API调用 + // 暂时初始化为空的评论列表 + discussionList.value = [] + console.log('✅ 讨论主题数据加载完成') + + // TODO: 这里可能需要调用另一个API来获取评论列表 + // 例如: loadDiscussionComments(firstDiscussion.id) + } else { console.warn('⚠️ 讨论数据格式异常:', discussionData) discussionList.value = [] + // 重置标题和描述 + discussionTitle.value = '讨论' + discussionDescription.value = '' } } else { console.error('❌ 获取章节讨论失败:', response.data?.message || response.message) @@ -2815,11 +2895,25 @@ const loadDiscussionData = async (section: CourseSection) => { } } +// 工具函数:去掉HTML标签,保留文本内容 +const stripHtmlTags = (html: string): string => { + if (!html) return '' + + // 创建一个临时的DOM元素来解析HTML + const tempDiv = document.createElement('div') + tempDiv.innerHTML = html + + // 获取纯文本内容 + return tempDiv.textContent || tempDiv.innerText || '' +} + const exitDiscussion = () => { console.log('🚪 正在退出讨论模式...') discussionMode.value = false currentDiscussionSection.value = null discussionList.value = [] + discussionTitle.value = '讨论' + discussionDescription.value = '' newComment.value = '' replyingTo.value = null console.log('✅ 已退出讨论模式,discussionMode:', discussionMode.value) diff --git a/src/views/ExamNotice.vue b/src/views/ExamNotice.vue index 5aad035..d290920 100644 --- a/src/views/ExamNotice.vue +++ b/src/views/ExamNotice.vue @@ -133,9 +133,9 @@ const examName = ref(route.query.examName as string || '考试') // const viewCount = ref(1024) // 暂时注释,后续需要时再启用 // 返回上级 -const goBack = () => { - router.push(`/course/${courseId.value}`) -} +// const goBack = () => { +// router.push(`/course/${courseId.value}`) +// } // 暂时未使用