diff --git a/backend/app/services/ai/knowledge_analysis_v2.py b/backend/app/services/ai/knowledge_analysis_v2.py index eafc93a..ea0eeef 100644 --- a/backend/app/services/ai/knowledge_analysis_v2.py +++ b/backend/app/services/ai/knowledge_analysis_v2.py @@ -176,7 +176,7 @@ class KnowledgeAnalysisServiceV2: """ 提取文档内容 - 支持:PDF、Word(docx)、文本文件 + 支持:PDF、Word(docx)、Excel(xlsx/xls)、文本文件 """ suffix = file_path.suffix.lower() @@ -185,6 +185,8 @@ class KnowledgeAnalysisServiceV2: return await self._extract_pdf_content(file_path) elif suffix in ['.docx', '.doc']: return await self._extract_docx_content(file_path) + elif suffix in ['.xlsx', '.xls']: + return await self._extract_excel_content(file_path) elif suffix in ['.txt', '.md', '.text']: return await self._extract_text_content(file_path) else: @@ -272,6 +274,35 @@ class KnowledgeAnalysisServiceV2: logger.error(f"文本文件读取失败: {e}") raise ValueError(f"文本文件读取失败: {e}") + async def _extract_excel_content(self, file_path: Path) -> str: + """提取 Excel 文件内容""" + try: + from openpyxl import load_workbook + + wb = load_workbook(str(file_path), read_only=True, data_only=True) + text_parts = [] + + for sheet_name in wb.sheetnames: + sheet = wb[sheet_name] + text_parts.append(f"【工作表: {sheet_name}】") + + for row in sheet.iter_rows(values_only=True): + # 过滤空行 + row_values = [str(cell) if cell is not None else '' for cell in row] + if any(v.strip() for v in row_values): + text_parts.append(' | '.join(row_values)) + + wb.close() + content = '\n'.join(text_parts) + return self._clean_content(content) + + except ImportError: + logger.error("openpyxl 未安装,无法读取 Excel 文件") + raise ValueError("服务器未安装 Excel 读取组件(openpyxl)") + except Exception as e: + logger.error(f"Excel 文件读取失败: {e}") + raise ValueError(f"Excel 文件读取失败: {e}") + def _clean_content(self, content: str) -> str: """清理和截断内容""" # 移除多余空白 diff --git a/frontend/src/views/manager/student-scores.vue b/frontend/src/views/manager/student-scores.vue index 0efb172..5341591 100644 --- a/frontend/src/views/manager/student-scores.vue +++ b/frontend/src/views/manager/student-scores.vue @@ -62,12 +62,14 @@ - - - - - - + + + @@ -323,6 +325,7 @@ import { ref, reactive, computed, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { getStudentScores, getStudentScoresStatistics, getExamMistakes, deleteExamRecord, type StudentScoreRecord, type MistakeRecord } from '@/api/manager/scores' +import { getManagerCourses } from '@/api/manager' // 加载状态 const loading = ref(false) @@ -350,10 +353,30 @@ const mistakesList = ref([]) const filterForm = reactive({ studentName: '', position: '', - course: '', + courseId: null as number | null, scoreRange: '' }) +// 课程列表 +const courseList = ref>([]) + +/** + * 加载课程列表 + */ +const loadCourseList = async () => { + try { + const res = await getManagerCourses({ page: 1, size: 100, status: 'published' }) + if (res.code === 200 && res.data) { + courseList.value = res.data.items.map(item => ({ + id: item.id, + name: item.name || item.title + })) + } + } catch (error) { + console.error('加载课程列表失败:', error) + } +} + // 成绩统计数据 const scoreStats = ref([ { @@ -498,6 +521,9 @@ const loadScoresList = async () => { if (filterForm.position) { params.position = getPositionText(filterForm.position) } + if (filterForm.courseId) { + params.course_id = filterForm.courseId + } if (filterForm.scoreRange) { params.score_range = filterForm.scoreRange } @@ -568,7 +594,7 @@ const handleSearch = () => { const handleReset = () => { filterForm.studentName = '' filterForm.position = '' - filterForm.course = '' + filterForm.courseId = null filterForm.scoreRange = '' dateRange.value = [ new Date(new Date().setDate(new Date().getDate() - 30)), @@ -700,6 +726,7 @@ const handleDeleteRecord = async (record: StudentScoreRecord) => { // 组件挂载时初始化数据 onMounted(() => { + loadCourseList() loadScoresList() loadStatistics() })