Files
education/frontend/src/views/student/studentInfo/index.vue
T
2026-09-10 17:42:46 +08:00

877 lines
36 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="app-container student-info-page">
<!-- ==================== 1. 查询条件区域 ==================== -->
<el-card v-if="!isStudentRole" shadow="never" class="search-card">
<el-form :model="searchForm" label-width="110px" class="search-form">
<el-row :gutter="24">
<!-- 左栏 -->
<el-col :xs="24" :md="12">
<el-form-item label="学号">
<el-input v-model="searchForm.xh" placeholder="请输入学号" clearable />
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="searchForm.xm" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="searchForm.xb" placeholder="请选择性别" clearable class="full-width">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="证件号码">
<el-input v-model="searchForm.zjhm" placeholder="请输入证件号码" clearable />
</el-form-item>
<el-form-item label="政治面貌">
<el-select v-model="searchForm.zzmm" placeholder="请选择政治面貌" clearable filterable class="full-width">
<el-option
v-for="item in politicalStatusOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
<el-form-item label="民族">
<el-select v-model="searchForm.mz" placeholder="请选择民族" clearable filterable class="full-width">
<el-option
v-for="item in nationOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
<el-form-item label="籍贯">
<el-input v-model="searchForm.jg" placeholder="请输入籍贯" clearable />
</el-form-item>
<el-form-item label="学位">
<el-select v-model="searchForm.xw" placeholder="请选择学位" clearable filterable class="full-width">
<el-option
v-for="item in degreeOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 右栏 -->
<el-col :xs="24" :md="12">
<el-form-item label="原部职别">
<el-input v-model="searchForm.ybzb" placeholder="请输入原部职别" clearable />
</el-form-item>
<el-form-item label="身份证号">
<el-input v-model="searchForm.sfzh" placeholder="请输入身份证号" clearable />
</el-form-item>
<el-form-item label="文化程度">
<el-select v-model="searchForm.whcd" placeholder="请选择文化程度" clearable filterable class="full-width">
<el-option
v-for="item in educationLevelOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
<el-form-item label="本科毕业院校">
<el-input v-model="searchForm.bkbyyx" placeholder="请输入本科毕业院校" clearable />
</el-form-item>
<el-form-item label="学员类别">
<el-select v-model="searchForm.xylb" placeholder="请选择学员类别" clearable filterable class="full-width">
<el-option
v-for="item in studentCategoryOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="searchForm.lxdh" placeholder="请输入联系电话" clearable />
</el-form-item>
<el-form-item label="通信地址">
<el-input v-model="searchForm.txdz" placeholder="请输入通信地址" clearable />
</el-form-item>
</el-col>
</el-row>
<div class="search-actions">
<el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
</div>
</el-form>
</el-card>
<!-- ==================== 2. 数据表格区域 ==================== -->
<el-card shadow="never" class="table-card">
<div class="list-header">
<div class="list-title">学员信息列表</div>
<el-button v-if="!isStudentRole" type="primary" icon="el-icon-plus" @click="handleAdd">新增学员</el-button>
</div>
<el-table v-loading="loading" :data="tableData" class="student-info-table" stripe border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="xh" label="学号" width="130" align="center" show-overflow-tooltip />
<el-table-column prop="xm" label="姓名" width="90" align="center" show-overflow-tooltip />
<el-table-column prop="xb" label="性别" width="70" align="center" />
<el-table-column prop="nj" label="年级" width="80" align="center" />
<el-table-column prop="zy" label="专业" width="160" align="center" show-overflow-tooltip />
<el-table-column prop="xylb" label="学员类别" width="110" align="center" show-overflow-tooltip />
<el-table-column prop="bc" label="班次" width="180" align="center" show-overflow-tooltip />
<el-table-column prop="zzmm" label="政治面貌" width="110" align="center" />
<el-table-column prop="mz" label="民族" width="80" align="center" />
<el-table-column prop="jg" label="籍贯" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="xw" label="学位" width="90" align="center" />
<el-table-column prop="zczt" label="注册状态" width="110" align="center" />
<el-table-column prop="xjyd" label="学籍异动状态" width="130" align="center" />
<el-table-column prop="ksqk" label="考试情况" width="100" align="center" />
<el-table-column prop="bjgm" label="不及格门数" width="110" align="center" />
<el-table-column prop="gljg" label="管理机构" width="150" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="190" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" class="text-primary" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" class="text-success" @click="handleGrades(row)">成绩</el-button>
<el-button type="text" class="danger-text" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
:current-page="pagination.pageNum"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog
:visible="dialogVisible"
:title="dialogTitle"
width="780px"
:close-on-click-modal="false"
@update:visible="val => dialogVisible = val"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<el-row v-if="dialogTitle !== '新增学员'" :gutter="20">
<el-col :span="12"><el-form-item label="编号" prop="bh"><el-input v-model="formData.bh" placeholder="请输入编号" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="学号" prop="xh"><el-input v-model="formData.xh" placeholder="请输入学号" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="姓名" prop="xm"><el-input v-model="formData.xm" placeholder="请输入姓名" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="性别" prop="xb"><el-select v-model="formData.xb" placeholder="请选择" class="full-width"><el-option label="男" value="1" /><el-option label="女" value="0" /></el-select></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="出生日期" prop="csrq"><el-date-picker v-model="formData.csrq" type="date" placeholder="请选择" value-format="yyyy-MM-dd" class="full-width" /></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="民族" prop="mz">
<el-select v-model="formData.mz" placeholder="请选择民族" filterable class="full-width">
<el-option
v-for="item in nationOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="籍贯" prop="jg"><el-input v-model="formData.jg" placeholder="请输入籍贯" /></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="政治面貌" prop="zzmm">
<el-select v-model="formData.zzmm" placeholder="请选择政治面貌" filterable class="full-width">
<el-option
v-for="item in politicalStatusOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="文化程度" prop="whcd">
<el-select v-model="formData.whcd" placeholder="请选择文化程度" filterable class="full-width">
<el-option
v-for="item in educationLevelOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学员类别" prop="xylb">
<el-select v-model="formData.xylb" placeholder="请选择学员类别" filterable class="full-width">
<el-option
v-for="item in studentCategoryOptions"
:key="item.dictCode || item.dictValue"
:label="item.dictLabel"
:value="item.dictLabel"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="学员队期编号" prop="xydqbh"><el-input v-model="formData.xydqbh" placeholder="请输入学员队期编号" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="证件号码" prop="zjhm"><el-input v-model="formData.zjhm" placeholder="请输入证件号码" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="所属军区" prop="ssjq"><el-input v-model="formData.ssjq" placeholder="请输入所属军区" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="当前班次编号" prop="dqbzbh"><el-input v-model="formData.dqbzbh" placeholder="请输入当前班次编号" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="军人证件类型" prop="jrzjlx"><el-input v-model="formData.jrzjlx" placeholder="请输入军人证件类型" /></el-form-item></el-col>
<el-col v-if="dialogTitle !== '新增学员'" :span="12"><el-form-item label="退学状态" prop="txzt"><el-input v-model="formData.txzt" placeholder="请输入退学状态" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="国防生" prop="gfs"><el-select v-model="formData.gfs" placeholder="请选择" class="full-width"><el-option label="是" value="1" /><el-option label="否" value="0" /></el-select></el-form-item></el-col>
<el-col v-if="dialogTitle !== '新增学员'" :span="8"><el-form-item label="留级状态" prop="ljzt"><el-input v-model="formData.ljzt" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="dialogTitle !== '新增学员'" :span="8"><el-form-item label="分班状态" prop="fbzt"><el-input v-model="formData.fbzt" placeholder="请输入" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="允许查看成绩" prop="yxckcj"><el-select v-model="formData.yxckcj" placeholder="请选择" class="full-width"><el-option label="是" value="1" /><el-option label="否" value="0" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="已注册" prop="yzc"><el-select v-model="formData.yzc" placeholder="请选择" class="full-width"><el-option label="是" value="1" /><el-option label="否" value="0" /></el-select></el-form-item></el-col>
<el-col :span="8" />
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="联系电话" prop="lxdh"><el-input v-model="formData.lxdh" placeholder="请输入联系电话" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="通讯地址" prop="txdz"><el-input v-model="formData.txdz" placeholder="请输入通讯地址" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24"><el-form-item label="备注" prop="bz"><el-input v-model="formData.bz" type="textarea" :rows="2" placeholder="请输入备注" /></el-form-item></el-col>
</el-row>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- 成绩弹窗 -->
<el-dialog
:visible="gradesVisible"
title="学员成绩"
width="80%"
:close-on-click-modal="false"
@update:visible="val => gradesVisible = val"
>
<div class="student-summary">
<span><strong>学员:</strong>{{ gradesStudent || '-' }}</span>
<span><strong>学号:</strong>{{ gradesStudentNo || '-' }}</span>
<div class="grades-year-filter">
<span class="filter-label">年度:</span>
<el-select v-model="gradesYear" size="small" clearable placeholder="全部年度">
<el-option
v-for="year in gradeYearOptions"
:key="year"
:label="year"
:value="year"
/>
</el-select>
</div>
</div>
<el-tabs v-model="gradesTab">
<el-tab-pane label="成绩" name="grades">
<el-table v-loading="gradesLoading" :data="filteredGradesData" stripe border max-height="420">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="nd" label="年度" width="85" align="center" />
<el-table-column prop="kmbh" label="科目编号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="klx" label="课类型" width="90" align="center" />
<el-table-column prop="kscj" label="考试成绩" width="90" align="center" />
<el-table-column prop="pscj" label="平时成绩" width="90" align="center" />
<el-table-column prop="zzcj" label="最终成绩" width="90" align="center" />
<el-table-column prop="qwxf" label="期望学分" width="90" align="center" />
<el-table-column prop="bkcj" label="补考成绩" width="90" align="center" />
<el-table-column prop="bkcs" label="补考次数" width="90" align="center" />
<el-table-column prop="ksqk" label="考试情况" width="100" align="center" />
<el-table-column label="考试结果" width="90" align="center">
<template slot-scope="{ row }">{{ formatPassStatus(row.ytg) }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="进程成绩" name="process">
<el-table v-loading="gradesLoading" :data="filteredProcessGradesData" stripe border max-height="420">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="nd" label="年度" width="85" align="center" />
<el-table-column prop="kmbh" label="科目编号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="klx" label="课类型" width="90" align="center" />
<el-table-column prop="yscj" label="原始成绩" width="100" align="center" />
<el-table-column prop="zzcj" label="最终成绩" width="90" align="center" />
<el-table-column prop="bkcj1" label="补考成绩1" width="100" align="center" />
<el-table-column prop="bkcj2" label="补考成绩2" width="100" align="center" />
<el-table-column prop="bkcj3" label="补考成绩3" width="100" align="center" />
<el-table-column prop="bkcs" label="补考次数" width="90" align="center" />
<el-table-column prop="ksqk" label="考试情况" width="100" align="center" />
</el-table>
</el-tab-pane>
</el-tabs>
<div slot="footer">
<el-button type="primary" :loading="gradesExporting" @click="handleExportGrades">
{{ gradesTab === 'grades' ? '导出成绩' : '导出进程成绩' }}
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listStudentRecord,
getStudentRecord,
addStudentRecord,
updateStudentRecord,
delStudentRecord,
listStudentGrades,
listStudentProcessGrades,
exportStudentGrades,
exportStudentProcessGrades
} from "@/api/studentRecords/studentRecords"
import { getDicts } from '@/api/system/dict/data'
import { optionselect } from '@/api/system/dict/type'
const POLITICAL_STATUS_DICT_CODE = 'sys_political_status'
const NATION_DICT_CODE = 'nation_type'
const DEGREE_DICT_CODE = 'sys_degree_type'
const EDUCATION_LEVEL_DICT_CODE = 'sys_edu_level'
const GRADES_VIEW_DISABLED_VALUES = new Set(['0', 'false', '否', '不允许'])
const RESIDENT_IDENTITY_CARD_TYPES = new Set(['居民身份证', '身份证'])
const IDENTITY_CARD_CHECK_CODES = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
const IDENTITY_CARD_WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const PHONE_NUMBER_PATTERN = /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8})$/
function isResidentIdentityCardType(certificateType) {
return RESIDENT_IDENTITY_CARD_TYPES.has(String(certificateType || '').trim())
}
function isValidIdentityCard(identityCard) {
const normalizedValue = String(identityCard || '').trim().toUpperCase()
if (!/^\d{17}[\dX]$/.test(normalizedValue)) {
return false
}
const year = Number(normalizedValue.slice(6, 10))
const month = Number(normalizedValue.slice(10, 12))
const day = Number(normalizedValue.slice(12, 14))
const birthDate = new Date(year, month - 1, day)
const isValidDate = birthDate.getFullYear() === year &&
birthDate.getMonth() === month - 1 && birthDate.getDate() === day
if (!isValidDate || birthDate > new Date()) {
return false
}
const checksum = IDENTITY_CARD_WEIGHTS.reduce((total, weight, index) => {
return total + Number(normalizedValue[index]) * weight
}, 0)
return IDENTITY_CARD_CHECK_CODES[checksum % 11] === normalizedValue[17]
}
export default {
name: 'StudentInfoIndex',
data() {
const validateCertificateNumber = (rule, value, callback) => {
if (value && isResidentIdentityCardType(this.formData.jrzjlx) && !isValidIdentityCard(value)) {
callback(new Error('请输入合法的18位居民身份证号码'))
return
}
callback()
}
const validateBirthDate = (rule, value, callback) => {
if (value && new Date(`${value}T00:00:00`) > new Date()) {
callback(new Error('出生日期不能晚于今天'))
return
}
callback()
}
const validatePhoneNumber = (rule, value, callback) => {
if (value && !PHONE_NUMBER_PATTERN.test(String(value).trim())) {
callback(new Error('请输入正确的手机号码或固定电话号码'))
return
}
callback()
}
return {
// ==================== 查询条件 ====================
// 仅保留后端 XYXX 实体支持的字段:/student-records/list 会按实体字段动态构建查询条件
searchForm: {
xh: '', xm: '', xb: '', zjhm: '', zzmm: '', mz: '', jg: '', xw: '',
ybzb: '', sfzh: '', whcd: '', bkbyyx: '', xylb: '', lxdh: '', txdz: ''
},
politicalStatusOptions: [],
nationOptions: [],
degreeOptions: [],
educationLevelOptions: [],
studentCategoryOptions: [],
// ==================== 数据表格 ====================
loading: false,
tableData: [],
pagination: { pageNum: 1, pageSize: 20, total: 0 },
// ==================== 新增/编辑弹窗 ====================
dialogVisible: false,
dialogTitle: '新增学员',
submitting: false,
formData: this.createEmptyForm(),
formRules: {
xm: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
xydqbh: [{ required: true, message: '请输入学员队期编号', trigger: 'blur' }],
zjhm: [
{ required: true, message: '请输入证件号码', trigger: 'blur' },
{ validator: validateCertificateNumber, trigger: 'blur' }
],
zzmm: [{ required: true, message: '请选择政治面貌', trigger: 'change' }],
xb: [{ required: true, message: '请选择性别', trigger: 'change' }],
csrq: [
{ required: true, message: '请选择出生日期', trigger: 'change' },
{ validator: validateBirthDate, trigger: 'change' }
],
mz: [{ required: true, message: '请选择民族', trigger: 'change' }],
jg: [{ required: true, message: '请输入籍贯', trigger: 'blur' }],
whcd: [{ required: true, message: '请选择文化程度', trigger: 'change' }],
xylb: [{ required: true, message: '请选择学员类别', trigger: 'change' }],
ssjq: [{ required: true, message: '请输入所属军区', trigger: 'blur' }],
gfs: [{ required: true, message: '请选择国防生', trigger: 'change' }],
yxckcj: [{ required: true, message: '请选择允许查看成绩', trigger: 'change' }],
dqbzbh: [{ required: true, message: '请输入当前班次编号', trigger: 'blur' }],
yzc: [{ required: true, message: '请选择已注册', trigger: 'change' }],
jrzjlx: [{ required: true, message: '请输入军人证件类型', trigger: 'blur' }],
lxdh: [{ validator: validatePhoneNumber, trigger: 'blur' }]
},
// ==================== 成绩弹窗 ====================
gradesVisible: false,
gradesTab: 'grades',
gradesData: [],
processGradesData: [],
gradesYear: '',
gradesStudent: '',
gradesStudentNo: '',
currentBh: '',
gradesLoading: false,
gradesExporting: false
}
},
computed: {
gradeYearOptions() {
const years = [...this.gradesData, ...this.processGradesData]
.map(item => item.nd)
.filter(year => year !== '' && year !== null && year !== undefined)
.map(String)
return [...new Set(years)].sort((firstYear, secondYear) => secondYear.localeCompare(firstYear))
},
/** 学员角色(后端角色键 STUDENT)隐藏条件查询栏与新增学员按钮 */
isStudentRole() {
const roles = this.$store.getters.roles
return Array.isArray(roles) && roles.includes('STUDENT')
},
filteredGradesData() {
return this.filterGradesByYear(this.gradesData)
},
filteredProcessGradesData() {
return this.filterGradesByYear(this.processGradesData)
}
},
created() {
this.loadStudentDictionaries()
},
mounted() {
this.loadData()
},
methods: {
/** 加载学员基础信息字典,业务字段按字典标签查询和提交。 */
async loadStudentDictionaries() {
const emptyResponse = { data: [] }
const [politicalStatusResponse, nationResponse, degreeResponse, educationLevelResponse, dictTypeResponse] =
await Promise.all([
getDicts(POLITICAL_STATUS_DICT_CODE).catch(() => emptyResponse),
getDicts(NATION_DICT_CODE).catch(() => emptyResponse),
getDicts(DEGREE_DICT_CODE).catch(() => emptyResponse),
getDicts(EDUCATION_LEVEL_DICT_CODE).catch(() => emptyResponse),
optionselect().catch(() => emptyResponse)
])
this.politicalStatusOptions = politicalStatusResponse.data || []
this.nationOptions = nationResponse.data || []
this.degreeOptions = degreeResponse.data || []
this.educationLevelOptions = educationLevelResponse.data || []
this.studentCategoryOptions = await this.loadNamedDictionary(dictTypeResponse.data || [], '学员类别')
this.mergeExistingDictionaryOptions()
},
/** 根据字典名称匹配类型编码,避免业务页面硬编码数据库配置。 */
async loadNamedDictionary(dictTypes, dictName) {
const dictType = dictTypes.find(item => item.dictName === dictName) ||
dictTypes.find(item => String(item.dictName || '').includes(dictName))
if (!dictType || !dictType.dictType) {
return []
}
try {
const response = await getDicts(dictType.dictType)
return response.data || []
} catch (error) {
return []
}
},
/** 字典缺项时保留列表已有值,避免历史数据无法作为查询条件。 */
mergeExistingDictionaryOptions() {
this.politicalStatusOptions = this.mergeFieldOptions(this.politicalStatusOptions, 'zzmm')
this.nationOptions = this.mergeFieldOptions(this.nationOptions, 'mz')
this.degreeOptions = this.mergeFieldOptions(this.degreeOptions, 'xw')
this.educationLevelOptions = this.mergeFieldOptions(this.educationLevelOptions, 'whcd')
this.studentCategoryOptions = this.mergeFieldOptions(this.studentCategoryOptions, 'xylb')
},
mergeFieldOptions(dictionaryOptions, fieldName) {
const options = dictionaryOptions.slice()
const existingLabels = new Set(options.map(item => String(item.dictLabel)))
this.tableData.forEach(row => {
const value = row[fieldName]
if (value === '' || value === null || value === undefined || existingLabels.has(String(value))) {
return
}
options.push({
dictLabel: String(value),
dictValue: String(value)
})
existingLabels.add(String(value))
})
return options
},
/** 创建空白表单 */
createEmptyForm() {
return {
bh: '', xh: '', xm: '', xydqbh: '', zjhm: '', zzmm: '', xb: '', csrq: '', mz: '', jg: '',
whcd: '', xylb: '', ssjq: '', gfs: '', txzt: '', ljzt: '', fbzt: '', yxckcj: '', dqbzbh: '', yzc: '',
jrzjlx: '', sfzh: '', lxdh: '', txdz: '', bz: '', dtsj: ''
}
},
// ==================== 列表加载 ====================
loadData() {
this.loading = true
const params = {
pageNum: this.pagination.pageNum,
pageSize: this.pagination.pageSize
}
this.buildQueryParams(params)
listStudentRecord(params).then(res => {
const data = res.data || {}
this.tableData = data.records || []
this.pagination.total = data.total || 0
this.mergeExistingDictionaryOptions()
this.loading = false
}).catch(() => {
this.tableData = []
this.pagination.total = 0
this.loading = false
})
},
/** 组装查询参数:仅传后端 XYXX 实体支持的字符串字段,空值忽略 */
buildQueryParams(params) {
const textFields = [
'xh', 'xm', 'zjhm', 'zzmm', 'mz', 'jg', 'xw', 'ybzb', 'sfzh', 'whcd', 'bkbyyx', 'xylb', 'lxdh', 'txdz'
]
textFields.forEach(key => {
const v = this.searchForm[key]
if (v !== '' && v !== null && v !== undefined) params[key] = v.trim()
})
if (this.searchForm.xb) params.xb = this.searchForm.xb
},
// ==================== 查询/分页 ====================
handleSearch() {
this.pagination.pageNum = 1
this.loadData()
},
handlePageChange(page) {
this.pagination.pageNum = page
this.loadData()
},
handleSizeChange(size) {
this.pagination.pageSize = size
this.pagination.pageNum = 1
this.loadData()
},
// ==================== 新增/编辑弹窗 ====================
resetForm() {
Object.assign(this.formData, this.createEmptyForm())
this.$nextTick(() => {
if (this.$refs.formRef) {
this.$refs.formRef.clearValidate()
}
})
},
handleAdd() {
this.dialogTitle = '新增学员'
this.resetForm()
this.dialogVisible = true
},
handleEdit(row) {
this.dialogTitle = '编辑学员'
this.resetForm()
if (!row.bh) {
Object.assign(this.formData, this.createEmptyForm(), row)
this.dialogVisible = true
return
}
getStudentRecord(row.bh).then(res => {
const data = res.data || {}
Object.assign(this.formData, this.createEmptyForm(), data)
this.dialogVisible = true
}).catch(() => {})
},
// ==================== 新增/编辑提交 ====================
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const isAdd = this.dialogTitle === '新增学员'
const payload = { ...this.formData }
if (isAdd) {
delete payload.bh
delete payload.xh
delete payload.txzt
delete payload.ljzt
delete payload.fbzt
}
const request = isAdd ? addStudentRecord(payload) : updateStudentRecord(payload)
request.then(() => {
this.$message.success(isAdd ? '新增成功' : '编辑成功')
this.dialogVisible = false
this.loadData()
}).catch(() => {
}).finally(() => {
this.submitting = false
})
})
},
// ==================== 删除 ====================
handleDelete(row) {
this.$confirm(`确定要删除"${row.xm}"吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return delStudentRecord(row.bh)
}).then(() => {
this.$message.success('删除成功')
this.loadData()
}).catch(() => {})
},
// ==================== 成绩弹窗 ====================
async handleGrades(row) {
if (this.isGradesViewDisabled(row.yxckcj)) {
this.$alert('不允许查看该学员成绩', '提示', {
confirmButtonText: '确定',
type: 'warning'
}).catch(() => {})
return
}
this.gradesStudent = row.xm || ''
this.gradesStudentNo = row.xh || ''
this.currentBh = row.bh || ''
this.gradesData = []
this.processGradesData = []
this.gradesYear = ''
this.gradesTab = 'grades'
this.gradesVisible = true
this.gradesLoading = true
const emptyResponse = { data: { records: [] } }
const query = { pageNum: 1, pageSize: 1000, xybh: this.currentBh }
try {
const [gradesResponse, processGradesResponse] = await Promise.all([
listStudentGrades(query).catch(() => emptyResponse),
listStudentProcessGrades(query).catch(() => emptyResponse)
])
this.gradesData = gradesResponse.data.records || []
this.processGradesData = processGradesResponse.data.records || []
} finally {
this.gradesLoading = false
}
},
formatPassStatus(value) {
if (value === 1 || value === '1') {
return '已通过'
}
if (value === 0 || value === '0') {
return '未通过'
}
return '-'
},
isGradesViewDisabled(value) {
return GRADES_VIEW_DISABLED_VALUES.has(String(value).trim().toLowerCase())
},
filterGradesByYear(grades) {
if (!this.gradesYear) {
return grades
}
return grades.filter(item => String(item.nd) === this.gradesYear)
},
downloadGradesFile(blob, fileName) {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = fileName
link.click()
URL.revokeObjectURL(link.href)
},
async handleExportGrades() {
if (!this.currentBh) {
this.$message.warning('缺少学员编号,无法导出成绩')
return
}
this.gradesExporting = true
try {
const isProcessGrades = this.gradesTab === 'process'
const request = isProcessGrades ? exportStudentProcessGrades : exportStudentGrades
const blob = await request({ xybh: this.currentBh })
const suffix = isProcessGrades ? '课程过程成绩' : '课程成绩'
this.downloadGradesFile(blob, `${this.gradesStudent || '学员'}_${suffix}.xlsx`)
this.$message.success(`${suffix}导出成功`)
} finally {
this.gradesExporting = false
}
}
}
}
</script>
<style scoped lang="scss">
.student-info-page {
.search-card {
.search-form {
.range-control {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
.el-input {
flex: 1;
}
.range-sep {
flex-shrink: 0;
font-size: 12px;
color: #606266;
}
}
.search-tip {
font-size: 12px;
color: #f56c6c;
margin-top: 8px;
margin-bottom: 8px;
}
.search-actions {
display: flex;
justify-content: flex-end;
padding-top: 12px;
border-top: 1px solid #ebeef5;
}
}
}
.table-card {
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.el-table {
width: 100%;
}
.student-info-table ::v-deep .cell {
white-space: nowrap;
word-break: keep-all;
}
}
.text-primary {
color: #409eff;
padding: 0;
}
.text-success {
color: #67c23a;
padding: 0;
}
.danger-text {
color: #f56c6c;
padding: 0;
}
.full-width {
width: 100%;
}
.student-summary {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 32px;
margin-bottom: 8px;
color: #303133;
line-height: 24px;
.grades-year-filter {
display: flex;
align-items: center;
margin-left: auto;
.filter-label {
flex-shrink: 0;
margin-right: 8px;
}
.el-select {
width: 160px;
}
}
}
}
</style>