Files
railwaysystem/build_ui/src/views/train/plan/components/EnrollmentPanel.vue
T

611 lines
23 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="enrollment-panel">
<!-- 检索与操作条 -->
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="78px">
<el-form-item label="学员姓名" prop="studentName">
<el-input
v-model="queryParams.studentName"
placeholder="请输入学员姓名"
clearable
style="width: 150px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="参训状态" prop="enrollmentStatus">
<el-select v-model="queryParams.enrollmentStatus" placeholder="全部状态" clearable style="width: 120px">
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="住宿需求" prop="accommodationRequired">
<el-select v-model="queryParams.accommodationRequired" placeholder="全部" clearable style="width: 110px">
<el-option label="需要住宿" :value="1" />
<el-option label="不住宿" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div style="margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;">
<div>
<span style="font-weight: bold; font-size: 14px; margin-right: 15px;">本期参训名单</span>
<el-tag size="small" type="info">总人数:{{ total }}</el-tag>
</div>
<div>
<el-button
v-hasPermi="['train:enrollment:add']"
type="primary"
icon="el-icon-plus"
size="mini"
:disabled="isPlanLocked"
@click="handleOpenAddModal"
>添加参训人员</el-button>
<el-button
v-hasPermi="['train:enrollment:import']"
type="success"
plain
icon="el-icon-upload2"
size="mini"
:disabled="isPlanLocked"
@click="handleOpenBatchModal"
>批量导入</el-button>
</div>
</div>
<!-- 参训学员列表 -->
<el-table v-loading="loading" :data="enrollmentList" border style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="姓名" align="center" prop="studentName" min-width="100" show-overflow-tooltip />
<el-table-column label="性别" align="center" prop="gender" width="65">
<template slot-scope="scope">
<span>{{ scope.row.gender || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="身份证号" align="center" prop="idCardMasked" min-width="170" show-overflow-tooltip>
<template slot-scope="scope">
<!-- 严格使用脱敏字段,不暴露明文 -->
<span>{{ scope.row.idCardMasked || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="选派单位" align="left" prop="orgName" min-width="140" show-overflow-tooltip />
<el-table-column label="住宿需求" align="center" prop="accommodationRequired" width="95">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.accommodationRequired === 1 ? 'warning' : 'info'">
{{ scope.row.accommodationRequired === 1 ? '需住宿' : '不住宿' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="参训状态" align="center" prop="enrollmentStatus" width="95">
<template slot-scope="scope">
<el-tag size="mini" :type="statusTagType(scope.row.enrollmentStatus)">
{{ statusLabel(scope.row.enrollmentStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" fixed="right">
<template slot-scope="scope">
<el-button
v-hasPermi="['train:enrollment:query']"
size="mini"
type="text"
icon="el-icon-document"
@click="handleViewDetail(scope.row)"
>档案</el-button>
<el-button
v-if="scope.row.enrollmentStatus === 0"
v-hasPermi="['train:report:confirm']"
size="mini"
type="text"
icon="el-icon-check"
:disabled="isPlanLocked"
@click="handleConfirmReport(scope.row)"
>报到</el-button>
<el-dropdown
v-if="scope.row.enrollmentStatus !== 4"
v-hasPermi="['train:enrollment:edit']"
size="mini"
trigger="click"
@command="cmd => handleStatusAction(cmd, scope.row)"
>
<el-button size="mini" type="text" icon="el-icon-more" :disabled="isPlanLocked">
状态<i class="el-icon-arrow-down el-icon--right" />
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="2" :disabled="scope.row.enrollmentStatus === 2">标记请假</el-dropdown-item>
<el-dropdown-item command="3" :disabled="scope.row.enrollmentStatus === 3">标记退训</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 开班前支持移除 -->
<el-button
v-if="canRemove(scope.row)"
v-hasPermi="['train:enrollment:remove']"
size="mini"
type="text"
icon="el-icon-delete"
style="color: #F56C6C;"
@click="handleRemove(scope.row)"
>移除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页器 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加单名参训人员对话框 (UC-04) -->
<el-dialog
title="添加单名参训人员"
:visible.sync="addDialog.open"
width="780px"
append-to-body
@close="closeAddDialog"
>
<div style="margin-bottom: 15px;">
<el-form :model="studentSearchQuery" size="small" :inline="true">
<el-form-item label="学员检索">
<el-input
v-model="studentSearchQuery.studentName"
placeholder="请输入姓名检索学员库"
clearable
style="width: 200px"
@keyup.enter.native="searchStudentPool"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="searchStudentPool">检索学员</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="addDialog.searchLoading"
:data="addDialog.studentCandidates"
highlight-current-row
height="220"
border
@current-change="handleSelectStudent"
>
<el-table-column label="选择" width="55" align="center">
<template slot-scope="scope">
<el-radio v-model="addDialog.selectedStudentId" :label="scope.row.sId" />
</template>
</el-table-column>
<el-table-column label="姓名" prop="studentName" min-width="100" />
<el-table-column label="性别" prop="gender" width="65" align="center" />
<el-table-column label="身份证号" prop="idCardMasked" min-width="170" show-overflow-tooltip />
<el-table-column label="选派单位" prop="orgName" min-width="140" show-overflow-tooltip />
</el-table>
</div>
<el-form ref="addForm" :model="addForm" :rules="addRules" label-width="110px" size="small">
<el-form-item label="选中学员" prop="studentId">
<el-input
:value="addDialog.selectedStudent ? (addDialog.selectedStudent.studentName + (addDialog.selectedStudent.orgName ? ' - ' + addDialog.selectedStudent.orgName : '')) : ''"
readonly
placeholder="请在上方列表点击选择学员"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="参训来源" prop="sourceType">
<el-select v-model="addForm.sourceType" placeholder="请选择参训来源" style="width: 100%">
<el-option label="管理员录入" :value="1" />
<el-option label="统一调训" :value="2" />
<el-option label="内部平台同步" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="住宿需求" prop="accommodationRequired">
<el-radio-group v-model="addForm.accommodationRequired">
<el-radio :label="1">需要住宿</el-radio>
<el-radio :label="0">不住宿</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注说明" prop="remark">
<el-input v-model="addForm.remark" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="选填入班备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="addDialog.submitting" @click="submitAddForm">确认添加</el-button>
<el-button @click="addDialog.open = false">取 消</el-button>
</div>
</el-dialog>
<!-- Excel 批量导入对话框 (UC-05) -->
<el-dialog
title="批量导入参训学员 (Excel)"
:visible.sync="batchDialog.open"
width="520px"
append-to-body
>
<el-form label-width="110px" size="small">
<el-form-item label="上传文件">
<el-upload
ref="upload"
action="#"
:auto-upload="false"
:limit="1"
accept=".xlsx, .xls"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
>
<el-button slot="trigger" size="small" type="primary" icon="el-icon-upload">选取 Excel 文件</el-button>
<div slot="tip" class="el-upload__tip">请上传 .xlsx 或 .xls 文件,单次最大支持 500 行数据。</div>
</el-upload>
</el-form-item>
<el-form-item label="学员档案策略">
<el-switch v-model="batchDialog.autoCreateStudent" active-text="学员不存在时自动建档" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button
type="primary"
:loading="batchDialog.submitting"
:disabled="!batchDialog.file"
@click="submitBatchImport"
>开始导入</el-button>
<el-button @click="batchDialog.open = false">关 闭</el-button>
</div>
</el-dialog>
<!-- 参训档案详情全景对话框 -->
<el-dialog
title="参训档案全景"
:visible.sync="detailDialog.open"
width="680px"
append-to-body
>
<div v-loading="detailDialog.loading">
<el-descriptions title="基础信息" :column="2" border size="small">
<el-descriptions-item label="参训编号">{{ detailDialog.data.enrollmentId || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划名称">{{ detailDialog.data.planName || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员姓名">{{ detailDialog.data.studentName || '-' }}</el-descriptions-item>
<el-descriptions-item label="学员学号">{{ detailDialog.data.studentId || '-' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detailDialog.data.gender || '-' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detailDialog.data.idCardMasked || '-' }}</el-descriptions-item>
<el-descriptions-item label="选派单位" :span="2">{{ detailDialog.data.orgName || '-' }}</el-descriptions-item>
<el-descriptions-item label="参训状态">
<el-tag size="mini" :type="statusTagType(detailDialog.data.enrollmentStatus)">
{{ statusLabel(detailDialog.data.enrollmentStatus) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="住宿需求">
{{ detailDialog.data.accommodationRequired === 1 ? '需要住宿' : '不住宿' }}
</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 15px;" />
<el-descriptions title="住宿安排" :column="2" border size="small">
<el-descriptions-item label="楼栋">{{ (detailDialog.data.accommodation && detailDialog.data.accommodation.building) || '未分配' }}</el-descriptions-item>
<el-descriptions-item label="房间">{{ (detailDialog.data.accommodation && detailDialog.data.accommodation.room) || '未分配' }}</el-descriptions-item>
<el-descriptions-item label="床位号">{{ (detailDialog.data.accommodation && detailDialog.data.accommodation.bedNo) || '未分配' }}</el-descriptions-item>
<el-descriptions-item label="门禁授权状态">
<el-tag size="mini" :type="detailDialog.data.accommodation && detailDialog.data.accommodation.authStatus === 1 ? 'success' : 'info'">
{{ (detailDialog.data.accommodation && detailDialog.data.accommodation.authStatus === 1) ? '已授权' : '待授权/无' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 15px;" />
<el-descriptions title="报到记录" :column="2" border size="small">
<el-descriptions-item label="报到时间">{{ (detailDialog.data.report && detailDialog.data.report.reportTime) || '未报到' }}</el-descriptions-item>
<el-descriptions-item label="报到方式">
<span v-if="detailDialog.data.report && detailDialog.data.report.reportWay === 1">人脸识别</span>
<span v-else-if="detailDialog.data.report && detailDialog.data.report.reportWay === 2">身份证刷卡</span>
<span v-else-if="detailDialog.data.report && detailDialog.data.report.reportWay === 3">二维码</span>
<span v-else-if="detailDialog.data.report && detailDialog.data.report.reportWay === 4">人工录入</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="核验说明" :span="2">{{ (detailDialog.data.report && detailDialog.data.report.verifyNote) || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detailDialog.open = false">关 闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listPlanEnrollments,
addPlanEnrollment,
importPlanEnrollments,
getEnrollmentDetail,
updateEnrollmentStatus,
removeEnrollment,
confirmReport
} from '@/api/train/enrollment'
import { listStudent } from '@/api/train/student'
export default {
name: 'EnrollmentPanel',
props: {
planId: {
type: [String, Number],
required: true
},
planStatus: {
type: Number,
default: 0
}
},
data() {
return {
loading: false,
enrollmentList: [],
total: 0,
statusOptions: [
{ value: 0, label: '待报到' },
{ value: 1, label: '已报到' },
{ value: 2, label: '请假' },
{ value: 3, label: '退训' },
{ value: 4, label: '已完成' }
],
queryParams: {
pageNum: 1,
pageSize: 10,
studentName: null,
enrollmentStatus: null,
accommodationRequired: null
},
// 单名添加学员对话框
addDialog: {
open: false,
searchLoading: false,
submitting: false,
selectedStudentId: null,
selectedStudent: null,
studentCandidates: []
},
studentSearchQuery: {
studentName: ''
},
addForm: {
studentId: '',
sourceType: 1,
accommodationRequired: 1,
remark: ''
},
addRules: {
studentId: [{ required: true, message: '请先在学员库中选择学员', trigger: 'change' }],
sourceType: [{ required: true, message: '请选择参训来源', trigger: 'change' }],
accommodationRequired: [{ required: true, message: '请选择住宿需求', trigger: 'change' }]
},
// 批量导入
batchDialog: {
open: false,
submitting: false,
file: null,
autoCreateStudent: true
},
// 档案详情
detailDialog: {
open: false,
loading: false,
data: {}
}
}
},
computed: {
isPlanLocked() {
return this.planStatus === 2 || this.planStatus === 3
}
},
watch: {
planId: {
immediate: true,
handler(val) {
if (val) {
this.getList()
}
}
}
},
methods: {
statusLabel(status) {
const found = this.statusOptions.find(item => item.value === status)
return found ? found.label : '未知'
},
statusTagType(status) {
const map = { 0: 'info', 1: 'success', 2: 'warning', 3: 'danger', 4: 'primary' }
return map[status] || 'info'
},
canRemove(row) {
// 班级未结束且处于待报到状态允许开班前移除
return this.planStatus === 0 && row.enrollmentStatus === 0
},
getList() {
if (!this.planId) return
this.loading = true
listPlanEnrollments(this.planId, this.queryParams)
.then(response => {
this.enrollmentList = response.rows || []
this.total = response.total || 0
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
// ---- 查看参训档案全景 ----
handleViewDetail(row) {
this.detailDialog.loading = true
this.detailDialog.open = true
getEnrollmentDetail(row.enrollmentId)
.then(res => {
this.detailDialog.data = res.data || row
})
.catch(() => {
this.detailDialog.data = row
})
.finally(() => {
this.detailDialog.loading = false
})
},
// ---- 变更参训状态 ----
handleStatusAction(command, row) {
const status = Number(command)
const label = status === 2 ? '请假' : '退训'
this.$prompt(`请输入将学员【${row.studentName}】标记为【${label}】的备注原因:`, '变更参训状态', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S+/,
inputErrorMessage: '备注原因不能为空'
}).then(({ value }) => {
return updateEnrollmentStatus(row.enrollmentId, {
enrollmentStatus: status,
remark: value
})
}).then(() => {
this.$modal.msgSuccess(`已标记为${label}`)
this.getList()
}).catch(() => {})
},
// ---- 开班前移除参训人员 ----
handleRemove(row) {
this.$modal.confirm(`确定要从本期培训班中移除学员【${row.studentName}】吗?`).then(() => {
return removeEnrollment(row.enrollmentId)
}).then(() => {
this.$modal.msgSuccess('移除成功')
this.getList()
}).catch(() => {})
},
// ---- 添加单名学员 ----
handleOpenAddModal() {
this.addDialog.selectedStudentId = null
this.addDialog.selectedStudent = null
this.addDialog.studentCandidates = []
this.studentSearchQuery.studentName = ''
this.addForm = {
studentId: '',
sourceType: 1,
accommodationRequired: 1,
remark: ''
}
this.addDialog.open = true
this.searchStudentPool()
},
searchStudentPool() {
this.addDialog.searchLoading = true
listStudent({
pageNum: 1,
pageSize: 20,
studentName: this.studentSearchQuery.studentName
}).then(res => {
this.addDialog.studentCandidates = res.rows || []
}).finally(() => {
this.addDialog.searchLoading = false
})
},
handleSelectStudent(row) {
if (row) {
this.addDialog.selectedStudentId = row.sId
this.addDialog.selectedStudent = row
this.addForm.studentId = row.sId
}
},
submitAddForm() {
this.$refs.addForm.validate(valid => {
if (!valid) return
this.addDialog.submitting = true
addPlanEnrollment(this.planId, {
studentId: this.addForm.studentId,
sourceType: this.addForm.sourceType,
accommodationRequired: this.addForm.accommodationRequired,
remark: this.addForm.remark
}).then(() => {
this.$modal.msgSuccess('成功添加入班')
this.addDialog.open = false
this.getList()
}).finally(() => {
this.addDialog.submitting = false
})
})
},
closeAddDialog() {
this.addDialog.selectedStudentId = null
this.addDialog.selectedStudent = null
},
// ---- Excel 批量导入 ----
handleOpenBatchModal() {
this.batchDialog.file = null
this.batchDialog.autoCreateStudent = true
this.batchDialog.open = true
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
},
handleFileChange(file) {
this.batchDialog.file = file.raw
},
handleFileRemove() {
this.batchDialog.file = null
},
submitBatchImport() {
if (!this.batchDialog.file) {
this.$modal.msgWarning('请先选择要导入的 Excel 文件')
return
}
const formData = new FormData()
formData.append('file', this.batchDialog.file)
formData.append('autoCreateStudent', this.batchDialog.autoCreateStudent)
this.batchDialog.submitting = true
importPlanEnrollments(this.planId, formData).then(res => {
this.$modal.msgSuccess(res.msg || '批量导入成功')
this.batchDialog.open = false
this.getList()
}).finally(() => {
this.batchDialog.submitting = false
})
},
handleConfirmReport(row) {
this.$confirm(`确认学员【${row.studentName}】现场报到到班?`, '报到确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
return confirmReport(row.enrollmentId, {
reportWay: 4,
verifyNote: '管理员人工核验到班'
})
}).then(() => {
this.$modal.msgSuccess('报到成功')
this.getList()
}).catch(() => {})
}
}
}
</script>
<style scoped>
.enrollment-panel {
padding: 5px 0;
}
</style>