feat: 新增Excel内容提取支持 & 修复成绩查询课程筛选
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
- 后端 knowledge_analysis_v2.py: 新增 _extract_excel_content 方法支持xlsx/xls文件 - 前端 student-scores.vue: 课程筛选改为动态加载,修复筛选参数传递
This commit is contained in:
@@ -176,7 +176,7 @@ class KnowledgeAnalysisServiceV2:
|
|||||||
"""
|
"""
|
||||||
提取文档内容
|
提取文档内容
|
||||||
|
|
||||||
支持:PDF、Word(docx)、文本文件
|
支持:PDF、Word(docx)、Excel(xlsx/xls)、文本文件
|
||||||
"""
|
"""
|
||||||
suffix = file_path.suffix.lower()
|
suffix = file_path.suffix.lower()
|
||||||
|
|
||||||
@@ -185,6 +185,8 @@ class KnowledgeAnalysisServiceV2:
|
|||||||
return await self._extract_pdf_content(file_path)
|
return await self._extract_pdf_content(file_path)
|
||||||
elif suffix in ['.docx', '.doc']:
|
elif suffix in ['.docx', '.doc']:
|
||||||
return await self._extract_docx_content(file_path)
|
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']:
|
elif suffix in ['.txt', '.md', '.text']:
|
||||||
return await self._extract_text_content(file_path)
|
return await self._extract_text_content(file_path)
|
||||||
else:
|
else:
|
||||||
@@ -272,6 +274,35 @@ class KnowledgeAnalysisServiceV2:
|
|||||||
logger.error(f"文本文件读取失败: {e}")
|
logger.error(f"文本文件读取失败: {e}")
|
||||||
raise ValueError(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:
|
def _clean_content(self, content: str) -> str:
|
||||||
"""清理和截断内容"""
|
"""清理和截断内容"""
|
||||||
# 移除多余空白
|
# 移除多余空白
|
||||||
|
|||||||
@@ -62,12 +62,14 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="课程">
|
<el-form-item label="课程">
|
||||||
<el-select v-model="filterForm.course" placeholder="请选择" clearable>
|
<el-select v-model="filterForm.courseId" placeholder="请选择课程" clearable style="width: 200px">
|
||||||
<el-option label="全部课程" value="" />
|
<el-option label="全部课程" :value="null" />
|
||||||
<el-option label="皮肤管理基础" value="skin_management" />
|
<el-option
|
||||||
<el-option label="美容产品知识" value="beauty_products" />
|
v-for="course in courseList"
|
||||||
<el-option label="客户沟通技巧" value="communication" />
|
:key="course.id"
|
||||||
<el-option label="轻医美项目" value="light_medical" />
|
:label="course.name"
|
||||||
|
:value="course.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="成绩范围">
|
<el-form-item label="成绩范围">
|
||||||
@@ -323,6 +325,7 @@
|
|||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { getStudentScores, getStudentScoresStatistics, getExamMistakes, deleteExamRecord, type StudentScoreRecord, type MistakeRecord } from '@/api/manager/scores'
|
import { getStudentScores, getStudentScoresStatistics, getExamMistakes, deleteExamRecord, type StudentScoreRecord, type MistakeRecord } from '@/api/manager/scores'
|
||||||
|
import { getManagerCourses } from '@/api/manager'
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -350,10 +353,30 @@ const mistakesList = ref<MistakeRecord[]>([])
|
|||||||
const filterForm = reactive({
|
const filterForm = reactive({
|
||||||
studentName: '',
|
studentName: '',
|
||||||
position: '',
|
position: '',
|
||||||
course: '',
|
courseId: null as number | null,
|
||||||
scoreRange: ''
|
scoreRange: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 课程列表
|
||||||
|
const courseList = ref<Array<{id: number, name: string}>>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载课程列表
|
||||||
|
*/
|
||||||
|
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([
|
const scoreStats = ref([
|
||||||
{
|
{
|
||||||
@@ -498,6 +521,9 @@ const loadScoresList = async () => {
|
|||||||
if (filterForm.position) {
|
if (filterForm.position) {
|
||||||
params.position = getPositionText(filterForm.position)
|
params.position = getPositionText(filterForm.position)
|
||||||
}
|
}
|
||||||
|
if (filterForm.courseId) {
|
||||||
|
params.course_id = filterForm.courseId
|
||||||
|
}
|
||||||
if (filterForm.scoreRange) {
|
if (filterForm.scoreRange) {
|
||||||
params.score_range = filterForm.scoreRange
|
params.score_range = filterForm.scoreRange
|
||||||
}
|
}
|
||||||
@@ -568,7 +594,7 @@ const handleSearch = () => {
|
|||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
filterForm.studentName = ''
|
filterForm.studentName = ''
|
||||||
filterForm.position = ''
|
filterForm.position = ''
|
||||||
filterForm.course = ''
|
filterForm.courseId = null
|
||||||
filterForm.scoreRange = ''
|
filterForm.scoreRange = ''
|
||||||
dateRange.value = [
|
dateRange.value = [
|
||||||
new Date(new Date().setDate(new Date().getDate() - 30)),
|
new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||||
@@ -700,6 +726,7 @@ const handleDeleteRecord = async (record: StudentScoreRecord) => {
|
|||||||
|
|
||||||
// 组件挂载时初始化数据
|
// 组件挂载时初始化数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadCourseList()
|
||||||
loadScoresList()
|
loadScoresList()
|
||||||
loadStatistics()
|
loadStatistics()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user