Files
education/frontend/src/views/student/statusChange/index.vue
T

731 lines
25 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 status-change-page">
<!-- ==================== 1. 查询条件 ==================== -->
<el-card shadow="never" class="search-card">
<el-form ref="searchFormRef" :model="searchForm" label-width="90px" class="search-form">
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="编号">
<el-input v-model="searchForm.bh" placeholder="请输入编号" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="学员编号">
<el-input v-model="searchForm.xybh" placeholder="请输入学员编号" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="申请类型">
<el-select v-model="searchForm.sqlx" placeholder="请选择申请类型" clearable class="w-full">
<el-option v-for="item in applyTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item label="状态">
<el-select v-model="searchForm.zt" placeholder="请选择状态" clearable class="w-full">
<el-option v-for="(item, key) in statusMap" :key="key" :label="item.label" :value="Number(key)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-actions">
<div class="search-buttons">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ==================== 2. 列表 ==================== -->
<el-card shadow="never" class="table-card">
<div class="table-header">
<span class="table-title">学籍异动申请列表</span>
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增学籍异动申请</el-button>
</div>
<el-table v-loading="loading" :data="tableData" stripe border highlight-current-row>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="bh" label="编号" min-width="140" show-overflow-tooltip />
<el-table-column prop="xybh" label="学员编号" min-width="120" show-overflow-tooltip />
<el-table-column prop="sqlx" label="申请类型" width="100" align="center" />
<el-table-column prop="sy" label="事由" min-width="140" show-overflow-tooltip />
<el-table-column prop="bz" label="备注" min-width="140" show-overflow-tooltip />
<el-table-column label="状态" width="90" align="center">
<template slot-scope="{ row }">
<el-tag :type="statusTagType(row.zt)" size="mini" effect="light">{{ statusLabel(row.zt) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="提交时间" width="160" align="center">
<template slot-scope="{ row }">{{ row.tjsj ? fmtDateTime(row.tjsj) : '-' }}</template>
</el-table-column>
<el-table-column label="创建时间" width="160" align="center">
<template slot-scope="{ row }">{{ row.cjsj ? fmtDateTime(row.cjsj) : '-' }}</template>
</el-table-column>
<el-table-column label="修改时间" width="160" align="center">
<template slot-scope="{ row }">{{ row.xgsj ? fmtDateTime(row.xgsj) : '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="230" align="center" fixed="right" class-name="table-action-column">
<template slot-scope="{ row }">
<el-button
v-if="!row.tjsj"
type="text"
size="small"
icon="el-icon-upload2"
@click="handleSubmit(row)"
>提交</el-button>
<el-button v-else type="text" size="small" icon="el-icon-check" disabled>已提交</el-button>
<el-button type="text" size="small" icon="el-icon-view" @click="handleDetail(row)">详情</el-button>
<el-button type="text" size="small" icon="el-icon-edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && tableData.length === 0" description="暂无学籍异动申请数据" :image-size="60" />
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination-wrapper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</el-card>
<!-- ==================== 3. 新增/编辑对话框 ==================== -->
<el-dialog
:visible="formDialogVisible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
@update:visible="val => formDialogVisible = val"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="110px" class="add-form">
<el-form-item label="学号" prop="xh">
<el-input
ref="studentNoInput"
v-model.trim="form.xh"
placeholder="请输入学号"
:disabled="studentVerifying"
@input="handleStudentNoInput"
@change="handleStudentNoComplete"
/>
<div v-if="isStudentVerified" class="student-verified-tip">
<i class="el-icon-circle-check" />
已核验{{ verifiedStudent.xm || verifiedStudent.xh || form.xh }}
</div>
</el-form-item>
<el-form-item label="申请类型" prop="sqlx">
<el-select v-model="form.sqlx" placeholder="请选择申请类型" class="w-full">
<el-option v-for="item in applyTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="事由" prop="sy">
<el-input v-model="form.sy" type="textarea" :rows="3" placeholder="请输入事由" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.bz" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="formDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="formSaving" @click="handleFormSubmit">确定</el-button>
</div>
</el-dialog>
<!-- ==================== 4. 学员信息核验对话框 ==================== -->
<el-dialog
title="请确认学员信息"
:visible="studentConfirmVisible"
width="560px"
append-to-body
:close-on-click-modal="false"
@update:visible="val => studentConfirmVisible = val"
>
<el-alert
title="请核对以下信息,确认无误后再提交申请。"
type="info"
:closable="false"
show-icon
class="student-confirm-alert"
/>
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ pendingStudent.xh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员编号">{{ pendingStudent.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ pendingStudent.xm || '-' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ studentSexLabel(pendingStudent.xb) }}</el-descriptions-item>
<el-descriptions-item label="学员队期编号">{{ pendingStudent.xydqbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="当前班次编号">{{ pendingStudent.dqbzbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员类别">{{ pendingStudent.xylb || '-' }}</el-descriptions-item>
<el-descriptions-item label="政治面貌">{{ pendingStudent.zzmm || '-' }}</el-descriptions-item>
</el-descriptions>
<div slot="footer">
<el-button @click="handleStudentReenter">重新输入</el-button>
<el-button type="primary" @click="handleStudentConfirm">确认无误</el-button>
</div>
</el-dialog>
<!-- ==================== 5. 详情对话框 ==================== -->
<el-dialog title="学籍异动申请详情" :visible="detailDialogVisible" width="760px" :close-on-click-modal="false"
@update:visible="val => detailDialogVisible = val">
<div v-loading="detailLoading" class="detail-body">
<el-descriptions :column="2" border>
<el-descriptions-item label="编号">{{ detailForm.bh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员编号">{{ detailForm.xybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请类型">{{ detailForm.sqlx || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusTagType(detailForm.zt)" size="mini">{{ statusLabel(detailForm.zt) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="事由" :span="2">{{ detailForm.sy || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailForm.bz || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailForm.cjsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="修改时间">{{ detailForm.xgsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ detailForm.tjsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队审核意见">{{ detailForm.xydshyj || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队干部编号">{{ detailForm.xydgbbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员队干部审核时间">{{ detailForm.xydshsj || '-' }}</el-descriptions-item>
<el-descriptions-item label="管理员账户编号">{{ detailForm.glyzhbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="管理员审核意见">{{ detailForm.glyshyj || '-' }}</el-descriptions-item>
<el-descriptions-item label="管理员审核时间">{{ detailForm.glyshsj || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listApplication,
getApplication,
addApplication,
updateApplication,
delApplication
} from '@/api/studentRecords/application'
import { getStudentRecord, listStudentRecord } from '@/api/studentRecords/studentRecords'
export default {
name: 'StatusChangeIndex',
computed: {
isStudentVerified() {
const studentNo = this.normalizeStudentNo(this.form.xh)
return Boolean(
studentNo &&
studentNo === this.verifiedStudentNo &&
this.form.xybh === this.verifiedStudent.bh
)
}
},
data() {
return {
// ==================== 申请类型选项(学籍异动) ====================
applyTypeOptions: ['休学', '复学', '退学', '转学', '留级', '转专业'],
// ==================== 状态映射:0-待审核 1-已审核 2-驳回等 ====================
statusMap: {
0: { label: '待审核', type: 'warning' },
1: { label: '已审核', type: 'success' },
2: { label: '驳回', type: 'danger' }
},
// ==================== 查询条件 ====================
searchForm: {
bh: '',
xybh: '',
sqlx: '',
zt: undefined
},
// ==================== 列表数据 ====================
loading: false,
tableData: [],
total: 0,
pageNum: 1,
pageSize: 20,
// ==================== 新增/编辑 ====================
formDialogVisible: false,
dialogTitle: '新增学籍异动申请',
isAdd: true,
formSaving: false,
studentVerifying: false,
studentConfirmVisible: false,
pendingStudent: {},
verifiedStudent: {},
verifiedStudentNo: '',
form: this.createEmptyForm(),
formRules: {
xh: [
{ required: true, message: '请输入学号', trigger: 'blur' },
{ max: 50, message: '学号不能超过50个字符', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9]+$/, message: '学号只能包含字母和数字', trigger: 'blur' }
],
sqlx: [{ required: true, message: '请选择申请类型', trigger: 'change' }],
sy: [{ required: true, message: '请输入事由', trigger: 'blur' }]
},
// ==================== 详情 ====================
detailDialogVisible: false,
detailLoading: false,
detailForm: {}
}
},
mounted() {
this.fetchList()
},
methods: {
createEmptyForm() {
return {
bh: '',
xh: '',
xybh: '',
sqlx: '',
sy: '',
bz: '',
zt: 0
}
},
// ==================== 状态展示 ====================
// 统一格式化后端 LocalDateTime(去除 T、截断到秒)
fmtDateTime(val) {
if (val === null || val === undefined || val === '') return ''
return String(val).replace('T', ' ').slice(0, 19)
},
statusLabel(zt) {
const item = this.statusMap[zt]
return item ? item.label : (zt !== null && zt !== undefined ? String(zt) : '-')
},
statusTagType(zt) {
const item = this.statusMap[zt]
return item ? item.type : 'info'
},
studentSexLabel(value) {
if (value === '' || value === null || value === undefined) {
return '-'
}
if (String(value) === '1') {
return '男'
}
if (String(value) === '0') {
return '女'
}
return String(value)
},
// ==================== 查询列表 ====================
fetchList() {
this.loading = true
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize
}
// 等值筛选,留空不传
;['bh', 'xybh', 'sqlx'].forEach(key => {
const value = this.searchForm[key]
if (value !== '' && value !== null && value !== undefined) {
params[key] = String(value).trim()
}
})
if (this.searchForm.zt !== undefined && this.searchForm.zt !== '' && this.searchForm.zt !== null) {
params.zt = this.searchForm.zt
}
listApplication(params).then(response => {
const data = response.data || {}
this.tableData = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
handleSearch() {
this.pageNum = 1
this.fetchList()
},
handleReset() {
this.$refs.searchFormRef.resetFields()
this.pageNum = 1
this.fetchList()
},
handlePageChange(current) {
this.pageNum = current
this.fetchList()
},
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
// ==================== 新增/编辑 ====================
handleAdd() {
this.isAdd = true
this.dialogTitle = '新增学籍异动申请'
this.form = this.createEmptyForm()
this.resetStudentVerification()
this.formDialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
handleEdit(row) {
this.isAdd = false
this.dialogTitle = '编辑学籍异动申请'
this.form = this.createEmptyForm()
this.resetStudentVerification()
this.formDialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
getApplication(row.bh).then(response => {
const data = response.data || {}
this.form = {
bh: data.bh || '',
xh: '',
xybh: data.xybh || '',
sqlx: data.sqlx || '',
sy: data.sy || '',
bz: data.bz || '',
zt: data.zt !== null && data.zt !== undefined ? Number(data.zt) : 0
}
this.loadExistingStudent(data.xybh)
}).catch(() => {})
},
normalizeStudentNo(value) {
return value === null || value === undefined ? '' : String(value).trim()
},
resetStudentVerification() {
this.studentVerifying = false
this.studentConfirmVisible = false
this.pendingStudent = {}
this.verifiedStudent = {}
this.verifiedStudentNo = ''
},
handleStudentNoInput() {
const studentNo = this.normalizeStudentNo(this.form.xh)
if (studentNo !== this.verifiedStudentNo) {
this.verifiedStudent = {}
this.verifiedStudentNo = ''
this.form.xybh = ''
}
this.pendingStudent = {}
},
handleStudentNoComplete() {
if (this.normalizeStudentNo(this.form.xh)) {
this.loadStudentForConfirmation()
}
},
async loadStudentForConfirmation() {
if (this.studentVerifying) {
return false
}
const studentNo = this.normalizeStudentNo(this.form.xh)
if (!studentNo) {
this.$refs.formRef && this.$refs.formRef.validateField('xh')
return false
}
if (studentNo === this.verifiedStudentNo && this.verifiedStudent.bh) {
this.pendingStudent = { ...this.verifiedStudent }
this.studentConfirmVisible = true
return true
}
this.studentVerifying = true
try {
const response = await listStudentRecord({ pageNum: 1, pageSize: 2, xh: studentNo })
const records = response.data && Array.isArray(response.data.records) ? response.data.records : []
// 请求返回前用户可能已经修改了学号,旧结果不能覆盖当前输入。
if (studentNo !== this.normalizeStudentNo(this.form.xh)) {
return false
}
if (records.length === 0) {
this.$message.warning('未查询到该学号对应的学员,请核对学号')
this.resetStudentVerification()
return false
}
if (records.length > 1) {
this.$message.warning('该学号匹配到多名学员,请联系管理员检查学员数据')
this.resetStudentVerification()
return false
}
this.pendingStudent = records[0]
this.studentConfirmVisible = true
return true
} catch (error) {
if (studentNo === this.normalizeStudentNo(this.form.xh)) {
this.resetStudentVerification()
}
return false
} finally {
this.studentVerifying = false
}
},
handleStudentConfirm() {
const studentNo = this.normalizeStudentNo(this.form.xh)
if (!this.pendingStudent.bh || this.normalizeStudentNo(this.pendingStudent.xh) !== studentNo) {
this.$message.warning('学号已变化,请重新核验')
this.studentConfirmVisible = false
return
}
this.verifiedStudent = { ...this.pendingStudent }
this.verifiedStudentNo = studentNo
this.form.xybh = this.pendingStudent.bh
this.studentConfirmVisible = false
this.$refs.formRef && this.$refs.formRef.clearValidate('xh')
this.$message.success('学员信息核验成功')
},
handleStudentReenter() {
this.studentConfirmVisible = false
this.pendingStudent = {}
this.verifiedStudent = {}
this.verifiedStudentNo = ''
this.form.xybh = ''
this.$nextTick(() => {
const input = this.$refs.studentNoInput
input && input.focus()
})
},
async loadExistingStudent(studentId) {
const normalizedStudentId = this.normalizeStudentNo(studentId)
if (!normalizedStudentId) {
return
}
this.studentVerifying = true
try {
const response = await getStudentRecord(normalizedStudentId)
const student = response.data || {}
if (this.form.xybh !== normalizedStudentId || !student.bh) {
return
}
this.form.xh = student.xh || ''
this.verifiedStudentNo = this.normalizeStudentNo(student.xh)
this.verifiedStudent = student
} catch (error) {
this.$message.warning('未能加载原申请的学员信息,请重新输入学号核验')
} finally {
this.studentVerifying = false
}
},
buildPayload() {
const f = this.form
const payload = {
xybh: f.xybh,
sqlx: f.sqlx,
sy: f.sy
}
// 新增编号和状态由后端生成;编辑时仅回传记录定位编号和原状态。
if (!this.isAdd) {
payload.bh = f.bh
if (f.zt !== null && f.zt !== undefined && f.zt !== '') {
payload.zt = Number(f.zt)
}
}
// 备注留空则移除
if (f.bz !== '' && f.bz !== null && f.bz !== undefined) {
payload.bz = f.bz
}
return payload
},
async handleFormSubmit() {
const isValid = await new Promise(resolve => {
this.$refs.formRef.validate(valid => resolve(valid))
})
if (!isValid) {
return
}
if (!this.isStudentVerified) {
const isFound = await this.loadStudentForConfirmation()
if (isFound) {
this.$message.warning('请先确认学员信息,再提交申请')
}
return
}
this.formSaving = true
const payload = this.buildPayload()
const request = this.isAdd ? addApplication(payload) : updateApplication(payload)
request.then(() => {
this.$message.success(this.isAdd ? '新增成功' : '修改成功')
this.formDialogVisible = false
this.fetchList()
}).catch(() => {
}).finally(() => {
this.formSaving = false
})
},
// ==================== 提交 ====================
// 无独立提交接口,通过编辑接口写入提交时间 tjsj(zt 保持原值不变)
formatNow() {
const d = new Date()
const pad = n => String(n).padStart(2, '0')
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
return `${date}T${time}`
},
handleSubmit(row) {
this.$confirm(`确定要提交编号为「${row.bh}」的学籍异动申请吗?`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const payload = {
bh: row.bh,
xybh: row.xybh,
sqlx: row.sqlx,
sy: row.sy,
zt: row.zt !== null && row.zt !== undefined ? Number(row.zt) : 0,
tjsj: this.formatNow()
}
if (row.bz) {
payload.bz = row.bz
}
return updateApplication(payload)
}).then(() => {
this.$message.success('提交成功')
this.fetchList()
}).catch(() => {})
},
// ==================== 删除 ====================
handleDelete(row) {
this.$confirm(`确定要删除编号为「${row.bh}」的学籍异动申请吗?`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return delApplication(row.bh)
}).then(() => {
this.$message.success('删除成功')
this.fetchList()
}).catch(() => {})
},
// ==================== 详情 ====================
handleDetail(row) {
this.detailDialogVisible = true
this.detailLoading = true
this.detailForm = {}
getApplication(row.bh).then(response => {
this.detailForm = response.data || {}
this.detailLoading = false
}).catch(() => {
this.detailLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.status-change-page {
.search-card {
margin-bottom: 16px;
}
.search-form {
.w-full {
width: 100%;
}
.search-actions {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 8px;
.search-tip {
font-size: 12px;
color: #909399;
}
.search-buttons {
display: flex;
gap: 12px;
}
}
}
.table-card {
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.table-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
.add-form {
max-height: 60vh;
overflow-y: auto;
padding-right: 4px;
}
.student-verified-tip {
margin-top: 4px;
color: #078267;
font-size: 12px;
line-height: 20px;
i {
margin-right: 4px;
}
}
.text-danger {
color: #f56c6c;
padding: 0;
}
}
.student-confirm-alert {
margin-bottom: 16px;
}
</style>