Files
school-developer/src/views/stuwork/stuconduct/indexTerm.vue
yaojian f7dee0da5e 1
2026-03-09 10:38:05 +08:00

567 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 搜索表单 -->
<el-row v-show="showSearch">
<el-form :model="queryForm" ref="searchFormRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="queryForm.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="queryForm.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 200px">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="班级" prop="classCode">
<el-select
v-model="queryForm.classCode"
placeholder="请选择班级"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
<el-button type="warning" icon="Bell" @click="handleSendWarning" :loading="warningLoading">发送预警</el-button>
<el-button type="success" icon="Download" @click="handleDownloadTemplate">导入模板</el-button>
<el-button type="primary" icon="Upload" @click="handleImport">导入考核</el-button>
</el-form-item>
</el-form>
</el-row>
<!-- 统计表格 -->
<el-row style="margin-bottom: 20px;">
<el-table
:data="statisticsData"
v-loading="loading"
border
style="width: 100%"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column prop="label" label="" width="120" align="center" fixed="left" />
<el-table-column prop="classNo" label="班级" min-width="150" align="center" />
<el-table-column prop="excellent" label="优秀" min-width="100" align="center" />
<el-table-column prop="good" label="良好" min-width="100" align="center" />
<el-table-column prop="pass" label="及格" min-width="100" align="center" />
<el-table-column prop="fail" label="不及格" min-width="100" align="center" />
</el-table>
</el-row>
<!-- 学生列表表格 -->
<el-row>
<el-table
:data="studentList"
v-loading="loading"
border
:max-height="600"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="stuNo" label="学号" show-overflow-tooltip align="center" />
<el-table-column prop="realName" label="姓名" show-overflow-tooltip align="center" />
<el-table-column prop="score" label="学期总评" show-overflow-tooltip align="center">
<template #default="scope">
<span>{{ scope.row.score !== null && scope.row.score !== undefined ? scope.row.score.toFixed(2) : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="scope">
<el-button
icon="View"
text
type="primary"
@click="handleView(scope.row)">
查看
</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
</div>
<!-- 查看详情弹窗接口queryDataByStuNo 通过学年学号查看详情按当前学期筛选 -->
<el-dialog
v-model="viewDialogVisible"
title="学期操行考核详情"
width="800px"
destroy-on-close
@close="viewDetailList = []">
<div v-if="viewRow" class="view-summary">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="学号">{{ viewRow.stuNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewRow.realName }}</el-descriptions-item>
<el-descriptions-item label="学年">{{ queryForm.schoolYear }}</el-descriptions-item>
<el-descriptions-item label="学期">{{ formatSchoolTerm(queryForm.schoolTerm) }}</el-descriptions-item>
<el-descriptions-item label="学期总评" :span="2">
{{ viewRow.score != null && viewRow.score !== undefined ? Number(viewRow.score).toFixed(2) : '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div class="view-detail-title">考核记录</div>
<el-table
:data="viewDetailList"
v-loading="viewLoading"
border
size="small"
max-height="400"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="schoolTerm" label="学期" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="recordDate" label="考核日期" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="conductType" label="类型" width="80" align="center">
<template #default="scope">
<el-tag :type="scope.row.conductType === '1' ? 'success' : 'danger'" size="small">
{{ scope.row.conductType === '1' ? '加分' : scope.row.conductType === '0' ? '扣分' : scope.row.conductType || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="score" label="分数" width="80" align="center">
<template #default="scope">
{{ scope.row.score != null && scope.row.score !== undefined ? Number(scope.row.score) : '-' }}
</template>
</el-table-column>
<el-table-column prop="description" label="情况记录" min-width="140" show-overflow-tooltip />
<el-table-column prop="remarks" label="备注" min-width="100" show-overflow-tooltip />
</el-table>
<template v-if="viewDetailList.length === 0 && !viewLoading">
<el-empty description="暂无考核记录" :image-size="80" />
</template>
</el-dialog>
<!-- 导入操行考核弹窗 -->
<el-dialog
title="导入操行考核数据"
v-model="importDialogVisible"
:width="500"
:close-on-click-modal="false"
draggable>
<el-upload
ref="uploadRef"
:auto-upload="false"
:on-change="handleFileChange"
:limit="1"
accept=".xlsx,.xls"
drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
只能上传 xlsx/xls 文件请先下载导入模板
</div>
</template>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleImportSubmit" :disabled="!importFile || importLoading">确认导入</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="StuConductTerm">
import { reactive, ref, onMounted, computed } from 'vue'
import { getStuConductTerm, queryDataByStuNo, sendConductWarning } from "/@/api/stuwork/stuconduct";
import { exportConductAssessmentTemplate, importConductAssessment, downloadBlobFile } from "/@/api/stuwork/file";
import { getClassListByRole } from "/@/api/basic/basicclass";
import { queryAllSchoolYear } from "/@/api/basic/basicyear";
import { getDicts } from "/@/api/admin/dict";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { UploadFilled } from '@element-plus/icons-vue'
// 表格样式 - 在组件内部定义,不从外部导入
const tableStyle = {
cellStyle: { padding: '8px 0' },
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// 定义变量内容
const searchFormRef = ref()
const uploadRef = ref()
const showSearch = ref(true)
const loading = ref(false)
const warningLoading = ref(false)
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const viewDialogVisible = ref(false)
const viewLoading = ref(false)
const viewRow = ref<any>(null)
const viewDetailList = ref<any[]>([])
const importDialogVisible = ref(false)
const importFile = ref<File | null>(null)
const importLoading = ref(false)
// 查询表单
const queryForm = reactive({
schoolYear: '',
schoolTerm: '',
classCode: ''
})
// 统计表格数据
const statisticsData = computed(() => {
if (studentList.value.length === 0) {
return []
}
// 计算各等级人数
// 优秀:>=90良好80-89及格60-79不及格<60
let excellent = 0 // 优秀
let good = 0 // 良好
let pass = 0 // 及格
let fail = 0 // 不及格
const total = studentList.value.length
studentList.value.forEach((student: any) => {
const score = student.score
if (score !== null && score !== undefined) {
if (score >= 90) {
excellent++
} else if (score >= 80) {
good++
} else if (score >= 60) {
pass++
} else {
fail++
}
}
})
// 计算比率
const excellentRate = total > 0 ? ((excellent / total) * 100).toFixed(2) + '%' : '0%'
const goodRate = total > 0 ? ((good / total) * 100).toFixed(2) + '%' : '0%'
const passRate = total > 0 ? ((pass / total) * 100).toFixed(2) + '%' : '0%'
const failRate = total > 0 ? ((fail / total) * 100).toFixed(2) + '%' : '0%'
// 优良率 = (优秀 + 良好) / 总人数
const excellentGoodCount = excellent + good
const excellentGoodRate = total > 0 ? ((excellentGoodCount / total) * 100).toFixed(2) + '%' : '0%'
// 获取班级名称
const classNo = studentList.value.length > 0 ? (studentList.value[0].classNo || '-') : '-'
return [
{
label: '人数',
classNo: classNo,
excellent: excellent,
good: good,
pass: pass,
fail: fail
},
{
label: '比率',
classNo: classNo,
excellent: excellentRate,
good: goodRate,
pass: passRate,
fail: failRate
},
{
label: '优良率',
classNo: classNo,
excellent: excellentGoodRate,
good: '-',
pass: '-',
fail: '-'
},
{
label: '备注',
classNo: classNo,
excellent: '-',
good: '-',
pass: '-',
fail: '-'
}
]
})
// 获取数据列表
const getDataList = async () => {
if (!queryForm.schoolYear || !queryForm.schoolTerm || !queryForm.classCode) {
useMessage().warning('请选择学年、学期和班级')
return
}
loading.value = true
try {
const res = await getStuConductTerm({
schoolYear: queryForm.schoolYear,
schoolTerm: queryForm.schoolTerm,
classCode: queryForm.classCode
})
if (res.data && Array.isArray(res.data)) {
// 处理返回的数据,提取学生列表
// 根据API文档返回的是StuConductTermVO数组
const tempList: any[] = []
res.data.forEach((item: any) => {
// 如果返回的数据结构中有basicStudentVOList需要展开
if (item.basicStudentVOList && Array.isArray(item.basicStudentVOList) && item.basicStudentVOList.length > 0) {
item.basicStudentVOList.forEach((student: any) => {
tempList.push({
stuNo: student.stuNo || item.stuNo,
realName: student.realName || item.realName,
score: item.score, // 学期总评分数
classNo: item.classNo,
classCode: item.classCode
})
})
} else {
// 直接使用item作为学生信息
tempList.push({
stuNo: item.stuNo,
realName: item.realName,
score: item.score,
classNo: item.classNo,
classCode: item.classCode
})
}
})
studentList.value = tempList
} else {
studentList.value = []
}
} catch (_err) {
studentList.value = []
} finally {
loading.value = false
}
}
// 格式化学期显示
const formatSchoolTerm = (value: string | number) => {
if (value === null || value === undefined || value === '') return '-'
const dictItem = schoolTermList.value.find((item: any) => item.value == value)
return dictItem ? dictItem.label : value
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
queryForm.schoolYear = ''
queryForm.schoolTerm = ''
queryForm.classCode = ''
studentList.value = []
}
// 查看详情接口GET /stuwork/stuconduct/queryDataByStuNo按当前学年+学号拉取后筛本学期记录)
const handleView = async (row: any) => {
if (!queryForm.schoolYear || !row.stuNo) {
useMessage().warning('缺少学年或学号')
return
}
viewRow.value = row
viewDialogVisible.value = true
viewDetailList.value = []
viewLoading.value = true
try {
const res = await queryDataByStuNo({
schoolYear: queryForm.schoolYear,
stuNo: row.stuNo
})
const list = Array.isArray(res.data) ? res.data : []
const term = queryForm.schoolTerm
viewDetailList.value = term ? list.filter((r: any) => String(r.schoolTerm) === String(term)) : list
} catch (_err) {
viewDetailList.value = []
} finally {
viewLoading.value = false
}
}
// 发送学期操行考核预警
const handleSendWarning = async () => {
if (!queryForm.schoolYear || !queryForm.schoolTerm) {
useMessage().warning('请先选择学年 and 学期')
return
}
const { confirm } = useMessageBox()
try {
await confirm(`确定要发送${queryForm.schoolYear}学年第${queryForm.schoolTerm === '1' ? '一' : '二'}学期操行考核预警吗将向班主任推送不及格学生低于60分的预警通知。`)
warningLoading.value = true
const res = await sendConductWarning(queryForm.schoolYear, queryForm.schoolTerm)
useMessage().success(res.msg || '预警通知发送成功')
} catch (err: any) {
if (err !== 'cancel') {
useMessage().error(err.msg || '预警通知发送失败')
}
} finally {
warningLoading.value = false
}
}
// 下载导入模板
const handleDownloadTemplate = async () => {
try {
await downloadBlobFile(exportConductAssessmentTemplate(), `操行考核导入模板_${Date.now()}.xlsx`)
} catch (err: any) {
useMessage().error(err?.msg || '下载模板失败')
}
}
// 打开导入弹窗
const handleImport = () => {
importDialogVisible.value = true
importFile.value = null
uploadRef.value?.clearFiles()
}
// 文件选择变化
const handleFileChange = (file: any) => {
importFile.value = file.raw
}
// 提交导入
const handleImportSubmit = async () => {
if (!importFile.value) {
useMessage().warning('请先选择要上传的文件')
return
}
importLoading.value = true
try {
const formData = new FormData()
formData.append('file', importFile.value)
await importConductAssessment(formData)
useMessage().success('导入成功')
importDialogVisible.value = false
importFile.value = null
uploadRef.value?.clearFiles()
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '导入失败')
} finally {
importLoading.value = false
}
}
// 获取学年列表
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
if (res.data && Array.isArray(res.data)) {
schoolYearList.value = res.data
} else {
schoolYearList.value = []
}
} catch (err) {
schoolYearList.value = []
}
}
// 获取学期字典
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
if (res.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
if (res.data && Array.isArray(res.data)) {
classList.value = res.data
} else {
classList.value = []
}
} catch (err) {
classList.value = []
}
}
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
getClassListData()
})
</script>
<style scoped lang="scss">
.layout-padding {
.layout-padding-auto {
.layout-padding-view {
.el-row {
margin-bottom: 20px;
}
}
}
}
.view-summary {
margin-bottom: 16px;
}
.view-detail-title {
margin-bottom: 8px;
font-weight: 600;
color: #303133;
}
// 确保页面可以滚动
.layout-padding {
height: 100%;
overflow-y: auto;
.layout-padding-auto {
height: 100%;
.layout-padding-view {
height: 100%;
overflow-y: auto;
}
}
}
</style>