新增培训:完善参训人员与排课前端
This commit is contained in:
1 parent
fb87c95506
commit
e65870ac6e
5 files changed
+1567
-3
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 3.1 查看本期参训名单 (UC-06) 【P0】
|
||||
export function listPlanEnrollments(planId, query) {
|
||||
return request({
|
||||
url: `/train/plans/${planId}/enrollments`,
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 3.2 添加单名参训人员 (UC-04) 【P0】
|
||||
export function addPlanEnrollment(planId, data) {
|
||||
return request({
|
||||
url: `/train/plans/${planId}/enrollments`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 3.3 Excel批量导入参训人员 (UC-05) 【P0】
|
||||
export function importPlanEnrollments(planId, data) {
|
||||
return request({
|
||||
url: `/train/plans/${planId}/enrollments/batch`,
|
||||
method: 'post',
|
||||
data: data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 3.4 获取参训档案详情 【P0】
|
||||
export function getEnrollmentDetail(enrollmentId) {
|
||||
return request({
|
||||
url: `/train/enrollments/${enrollmentId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 3.5 变更参训状态 【P1】
|
||||
export function updateEnrollmentStatus(enrollmentId, data) {
|
||||
return request({
|
||||
url: `/train/enrollments/${enrollmentId}/status`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 3.6 移除参训人员 【P1】
|
||||
export function removeEnrollment(enrollmentId) {
|
||||
return request({
|
||||
url: `/train/enrollments/${enrollmentId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 4.5 获取计划完整课表 (UC-11) 【P0】
|
||||
export function getPlanTimetable(planId) {
|
||||
return request({
|
||||
url: `/train/plans/${planId}/timetable`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 4.1 新增课次 (UC-07) 【P0】
|
||||
export function addLesson(planId, data) {
|
||||
return request({
|
||||
url: `/train/plans/${planId}/lessons`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 4.2 修改课次 (UC-08) 【P0】
|
||||
export function updateLesson(lessonId, data) {
|
||||
return request({
|
||||
url: `/train/lessons/${lessonId}`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 4.3 删除课次 (UC-09) 【P1】
|
||||
export function removeLesson(lessonId) {
|
||||
return request({
|
||||
url: `/train/lessons/${lessonId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 4.4 排课冲突检查接口 (UC-10) 【P0】
|
||||
export function checkLessonConflict(data) {
|
||||
return request({
|
||||
url: '/train/lessons/check-conflict',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
<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 label="参训编号" align="center" prop="enrollmentId" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="学员编号" align="center" prop="studentId" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="姓名" align="center" prop="studentName" min-width="95" 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-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="sId" min-width="100" show-overflow-tooltip />
|
||||
<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.sId})` : ''"
|
||||
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>-</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
|
||||
} 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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.enrollment-panel {
|
||||
padding: 5px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,755 @@
|
||||
<template>
|
||||
<div class="timetable-panel">
|
||||
<!-- 头部操作与统计条 -->
|
||||
<div style="margin-bottom: 15px; 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">总课次:{{ allLessons.length }} 节</el-tag>
|
||||
<span v-if="timetableData.startDate && timetableData.endDate" style="margin-left: 12px; font-size: 12px; color: #909399;">
|
||||
排课周期:{{ timetableData.startDate }} ~ {{ timetableData.endDate }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-radio-group v-model="viewMode" size="mini" style="margin-right: 10px;">
|
||||
<el-radio-button label="timeline">按日日程</el-radio-button>
|
||||
<el-radio-button label="table">课次明细</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:add']"
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
:disabled="isPlanLocked"
|
||||
@click="handleOpenAddLesson"
|
||||
>新增课次排课</el-button>
|
||||
<el-button
|
||||
icon="el-icon-refresh"
|
||||
size="mini"
|
||||
@click="loadTimetable"
|
||||
>刷新课表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视图一:按日日程视图 -->
|
||||
<div v-if="viewMode === 'timeline'" v-loading="loading" class="timeline-view">
|
||||
<div v-if="!timetableData.days || timetableData.days.length === 0" class="empty-tip">
|
||||
<el-empty description="暂未编排任何课次,请点击右上角【新增课次排课】" />
|
||||
</div>
|
||||
|
||||
<div v-else class="day-cards">
|
||||
<el-card
|
||||
v-for="day in timetableData.days"
|
||||
:key="day.date"
|
||||
class="day-card"
|
||||
shadow="hover"
|
||||
>
|
||||
<div slot="header" class="day-card-header">
|
||||
<span style="font-weight: bold; font-size: 14px; color: #303133;">
|
||||
<i class="el-icon-date" style="margin-right: 6px; color: #409EFF;" />
|
||||
{{ day.date }}
|
||||
<el-tag size="mini" type="info" style="margin-left: 8px;">{{ formatDayOfWeek(day.dayOfWeek) }}</el-tag>
|
||||
</span>
|
||||
<span style="font-size: 12px; color: #909399;">
|
||||
共 {{ (day.lessons && day.lessons.length) || 0 }} 节课
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!day.lessons || day.lessons.length === 0" style="color: #c0c4cc; font-size: 13px; padding: 10px 0;">
|
||||
本日无课程安排
|
||||
</div>
|
||||
|
||||
<div v-else class="lesson-items">
|
||||
<div
|
||||
v-for="lesson in day.lessons"
|
||||
:key="lesson.lessonId"
|
||||
class="lesson-item-box"
|
||||
>
|
||||
<div class="lesson-time">
|
||||
<i class="el-icon-time" style="margin-right: 4px;" />
|
||||
{{ formatTime(lesson.startTime) }} - {{ formatTime(lesson.endTime) }}
|
||||
</div>
|
||||
<div class="lesson-main">
|
||||
<div class="lesson-title">
|
||||
{{ lesson.courseName }}
|
||||
</div>
|
||||
<div class="lesson-meta">
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-user" /> 教师:{{ lesson.teacherName || '-' }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-location-outline" />
|
||||
场地:
|
||||
<el-tag size="mini" :type="lesson.venueType === 1 ? 'primary' : 'warning'">
|
||||
{{ lesson.venueType === 1 ? '教室' : '实训场地' }}
|
||||
</el-tag>
|
||||
{{ lesson.venueName || lesson.classRoomName || lesson.siteName || '-' }}
|
||||
</span>
|
||||
<span v-if="lesson.boardSn" class="meta-item">
|
||||
<i class="el-icon-monitor" /> 班牌:{{ lesson.boardSn }}
|
||||
</span>
|
||||
<span v-if="lesson.remark" class="meta-item" style="color: #909399;">
|
||||
<i class="el-icon-info" /> {{ lesson.remark }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lesson-actions">
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:edit']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
:disabled="isPlanLocked"
|
||||
@click="handleEditLesson(lesson)"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:remove']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
style="color: #F56C6C;"
|
||||
:disabled="isPlanLocked"
|
||||
@click="handleRemoveLesson(lesson)"
|
||||
>删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视图二:平面明细表格视图 -->
|
||||
<div v-else v-loading="loading">
|
||||
<el-table :data="allLessons" border style="width: 100%">
|
||||
<el-table-column label="课次ID" prop="lessonId" width="90" align="center" />
|
||||
<el-table-column label="课程名称" prop="courseName" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="授课教师" prop="teacherName" width="100" align="center" />
|
||||
<el-table-column label="开始时间" prop="startTime" width="165" align="center" />
|
||||
<el-table-column label="结束时间" prop="endTime" width="165" align="center" />
|
||||
<el-table-column label="场地类型" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="scope.row.venueType === 1 ? 'primary' : 'warning'">
|
||||
{{ scope.row.venueType === 1 ? '教室' : '实训场地' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上课地点" min-width="140" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.venueName || scope.row.classRoomName || scope.row.siteName || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="电子班牌" prop="boardSn" width="120" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.boardSn || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="课次说明" prop="remark" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" width="130" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:edit']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
:disabled="isPlanLocked"
|
||||
@click="handleEditLesson(scope.row)"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:remove']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
style="color: #F56C6C;"
|
||||
:disabled="isPlanLocked"
|
||||
@click="handleRemoveLesson(scope.row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 新增 / 编辑课次对话框 (UC-07, UC-08) -->
|
||||
<el-dialog
|
||||
:title="dialog.title"
|
||||
:visible.sync="dialog.open"
|
||||
width="680px"
|
||||
append-to-body
|
||||
@close="closeDialog"
|
||||
>
|
||||
<el-form ref="lessonForm" :model="lessonForm" :rules="rules" label-width="110px" size="small">
|
||||
<el-form-item label="本期课程" prop="planCourseId">
|
||||
<el-select
|
||||
v-model="lessonForm.planCourseId"
|
||||
placeholder="请选择本期配置的课程"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in planCourseOptions"
|
||||
:key="c.planCourseId"
|
||||
:label="`${c.cName || c.courseName} (${c.cCode || '无编码'}) - ${c.cDuration || 0}学时`"
|
||||
:value="c.planCourseId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="授课教师" prop="tId">
|
||||
<el-select
|
||||
v-model="lessonForm.tId"
|
||||
placeholder="请选择授课教师"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="triggerAutoConflictCheck"
|
||||
>
|
||||
<el-option
|
||||
v-for="t in teacherOptions"
|
||||
:key="t.tId"
|
||||
:label="`${t.name || t.tName} (${t.tId}) - ${t.workDept || t.orgName || '基地'}`"
|
||||
:value="t.tId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="课次开始" prop="startTime">
|
||||
<el-date-picker
|
||||
v-model="lessonForm.startTime"
|
||||
type="datetime"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="选择课次开始时间"
|
||||
style="width: 100%"
|
||||
@change="triggerAutoConflictCheck"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="课次结束" prop="endTime">
|
||||
<el-date-picker
|
||||
v-model="lessonForm.endTime"
|
||||
type="datetime"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="选择课次结束时间"
|
||||
style="width: 100%"
|
||||
@change="triggerAutoConflictCheck"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="场地类型" prop="venueType">
|
||||
<el-radio-group v-model="lessonForm.venueType" @change="handleVenueTypeChange">
|
||||
<el-radio :label="1">普通教学教室</el-radio>
|
||||
<el-radio :label="2">专业实训场地</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 教室与场地二选一 -->
|
||||
<el-form-item
|
||||
v-if="lessonForm.venueType === 1"
|
||||
label="教学教室"
|
||||
prop="classRoomId"
|
||||
>
|
||||
<el-select
|
||||
v-model="lessonForm.classRoomId"
|
||||
placeholder="请选择普通教室"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="triggerAutoConflictCheck"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in classroomOptions"
|
||||
:key="item.classRoomId"
|
||||
:label="`${item.roomName} (${item.roomLocation || ''}) [容量: ${item.capacity}人]`"
|
||||
:value="item.classRoomId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="lessonForm.venueType === 2"
|
||||
label="实训场地"
|
||||
prop="siteId"
|
||||
>
|
||||
<el-select
|
||||
v-model="lessonForm.siteId"
|
||||
placeholder="请选择专业实训场地"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="triggerAutoConflictCheck"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in siteOptions"
|
||||
:key="item.siteId"
|
||||
:label="`${item.siteName} (${item.siteLocation || ''}) [容量: ${item.capacity}人]`"
|
||||
:value="item.siteId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="课次备注" prop="remark">
|
||||
<el-input
|
||||
v-model="lessonForm.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
placeholder="请输入课次相关注意事项或说明(选填)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 冲突预检结果展示区域 (UC-10) -->
|
||||
<div style="margin-left: 110px; margin-bottom: 15px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-connection"
|
||||
:loading="conflictChecking"
|
||||
@click="handleManualCheckConflict"
|
||||
>排课冲突实时预检</el-button>
|
||||
<span style="font-size: 12px; color: #909399; margin-left: 10px;">选择教师、场地与时间后可实时检测冲突</span>
|
||||
</div>
|
||||
|
||||
<div v-if="conflictResult">
|
||||
<el-alert
|
||||
v-if="conflictResult.hasConflict"
|
||||
:title="`排课冲突:${conflictResult.conflictMessage || conflictTypeLabel(conflictResult.conflictType)}`"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<div v-if="conflictResult.conflictingLessons && conflictResult.conflictingLessons.length > 0" style="margin-top: 5px; font-size: 12px;">
|
||||
冲突课次明细:
|
||||
<div v-for="(item, idx) in conflictResult.conflictingLessons" :key="idx">
|
||||
- {{ item.courseName || '课次' }} ({{ item.startTime }} ~ {{ item.endTime }})
|
||||
</div>
|
||||
</div>
|
||||
</el-alert>
|
||||
<el-alert
|
||||
v-else
|
||||
title="排课预检通过:教师、场地与班级时间段无重叠冲突"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="dialog.submitting"
|
||||
@click="submitLessonForm"
|
||||
>保存课次</el-button>
|
||||
<el-button @click="dialog.open = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getPlanTimetable,
|
||||
addLesson,
|
||||
updateLesson,
|
||||
removeLesson,
|
||||
checkLessonConflict
|
||||
} from '@/api/train/timetable'
|
||||
import { listPlanCourses } from '@/api/train/planCourse'
|
||||
import { listTeacher } from '@/api/train/teacher'
|
||||
import { listClassroom } from '@/api/train/classroom'
|
||||
import { listSite } from '@/api/train/site'
|
||||
|
||||
export default {
|
||||
name: 'TimetablePanel',
|
||||
props: {
|
||||
planId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
planStatus: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
viewMode: 'timeline', // 'timeline' 或 'table'
|
||||
timetableData: {
|
||||
planId: null,
|
||||
planName: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
days: []
|
||||
},
|
||||
// 基础下拉选项缓存
|
||||
planCourseOptions: [],
|
||||
teacherOptions: [],
|
||||
classroomOptions: [],
|
||||
siteOptions: [],
|
||||
// 新增/编辑弹窗
|
||||
dialog: {
|
||||
open: false,
|
||||
title: '新增课次排课',
|
||||
isEdit: false,
|
||||
submitting: false
|
||||
},
|
||||
lessonForm: {
|
||||
lessonId: null,
|
||||
planCourseId: null,
|
||||
tId: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
venueType: 1,
|
||||
classRoomId: null,
|
||||
siteId: null,
|
||||
remark: ''
|
||||
},
|
||||
rules: {
|
||||
planCourseId: [{ required: true, message: '请选择本期课程', trigger: 'change' }],
|
||||
tId: [{ required: true, message: '请选择授课教师', trigger: 'change' }],
|
||||
startTime: [{ required: true, message: '课次开始时间不能为空', trigger: 'change' }],
|
||||
endTime: [{ required: true, message: '课次结束时间不能为空', trigger: 'change' }],
|
||||
venueType: [{ required: true, message: '请选择场地类型', trigger: 'change' }],
|
||||
classRoomId: [{ required: true, message: '请选择普通教室', trigger: 'change' }],
|
||||
siteId: [{ required: true, message: '请选择实训场地', trigger: 'change' }]
|
||||
},
|
||||
// 冲突检测状态与结果
|
||||
conflictChecking: false,
|
||||
conflictResult: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPlanLocked() {
|
||||
return this.planStatus === 2 || this.planStatus === 3
|
||||
},
|
||||
allLessons() {
|
||||
if (!this.timetableData.days) return []
|
||||
const list = []
|
||||
this.timetableData.days.forEach(day => {
|
||||
if (day.lessons && day.lessons.length) {
|
||||
day.lessons.forEach(l => list.push(l))
|
||||
}
|
||||
})
|
||||
return list
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
planId: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.loadTimetable()
|
||||
this.loadOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatDayOfWeek(num) {
|
||||
const map = { 1: '星期一', 2: '星期二', 3: '星期三', 4: '星期四', 5: '星期五', 6: '星期六', 7: '星期日' }
|
||||
return map[num] || ''
|
||||
},
|
||||
formatTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return ''
|
||||
if (dateTimeStr.includes(' ')) {
|
||||
return dateTimeStr.split(' ')[1].substring(0, 5)
|
||||
}
|
||||
return dateTimeStr
|
||||
},
|
||||
conflictTypeLabel(type) {
|
||||
const map = {
|
||||
'NONE': '无冲突',
|
||||
'TEACHER': '教师时间冲突',
|
||||
'CLASSROOM': '教室时间占用冲突',
|
||||
'SITE': '实训场地占用冲突',
|
||||
'PLAN_OVERLAP': '本班课次时段重叠'
|
||||
}
|
||||
return map[type] || '存在排课冲突'
|
||||
},
|
||||
// ---- 加载课表 ----
|
||||
loadTimetable() {
|
||||
if (!this.planId) return
|
||||
this.loading = true
|
||||
getPlanTimetable(this.planId)
|
||||
.then(res => {
|
||||
this.timetableData = res.data || { days: [] }
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
// ---- 预加载基础选项 ----
|
||||
loadOptions() {
|
||||
if (!this.planId) return
|
||||
listPlanCourses(this.planId).then(res => {
|
||||
this.planCourseOptions = res.data || []
|
||||
})
|
||||
listTeacher({ pageNum: 1, pageSize: 100 }).then(res => {
|
||||
this.teacherOptions = res.rows || []
|
||||
})
|
||||
listClassroom({ pageNum: 1, pageSize: 100 }).then(res => {
|
||||
this.classroomOptions = res.rows || []
|
||||
})
|
||||
listSite({ pageNum: 1, pageSize: 100 }).then(res => {
|
||||
this.siteOptions = res.rows || []
|
||||
})
|
||||
},
|
||||
handleVenueTypeChange(val) {
|
||||
if (val === 1) {
|
||||
this.lessonForm.siteId = null
|
||||
} else {
|
||||
this.lessonForm.classRoomId = null
|
||||
}
|
||||
this.triggerAutoConflictCheck()
|
||||
},
|
||||
// ---- 打开新增排课 ----
|
||||
handleOpenAddLesson() {
|
||||
this.dialog.isEdit = false
|
||||
this.dialog.title = '新增课次排课'
|
||||
this.resetLessonForm()
|
||||
this.dialog.open = true
|
||||
this.conflictResult = null
|
||||
},
|
||||
// ---- 打开修改排课 (不发明 lesson detail API,直接从已有VO回显) ----
|
||||
handleEditLesson(row) {
|
||||
this.dialog.isEdit = true
|
||||
this.dialog.title = '编辑课次信息'
|
||||
this.lessonForm = {
|
||||
lessonId: row.lessonId,
|
||||
planCourseId: row.planCourseId,
|
||||
tId: row.tId,
|
||||
startTime: row.startTime,
|
||||
endTime: row.endTime,
|
||||
venueType: row.venueType || 1,
|
||||
classRoomId: row.classRoomId || null,
|
||||
siteId: row.siteId || null,
|
||||
remark: row.remark || ''
|
||||
}
|
||||
this.conflictResult = null
|
||||
this.dialog.open = true
|
||||
},
|
||||
resetLessonForm() {
|
||||
this.lessonForm = {
|
||||
lessonId: null,
|
||||
planCourseId: null,
|
||||
tId: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
venueType: 1,
|
||||
classRoomId: null,
|
||||
siteId: null,
|
||||
remark: ''
|
||||
}
|
||||
if (this.$refs.lessonForm) {
|
||||
this.$refs.lessonForm.clearValidate()
|
||||
}
|
||||
},
|
||||
closeDialog() {
|
||||
this.conflictResult = null
|
||||
},
|
||||
// ---- 冲突预检逻辑 (UC-10) ----
|
||||
triggerAutoConflictCheck() {
|
||||
// 当教师、场地与时间均选择后自动静默预检
|
||||
const f = this.lessonForm
|
||||
const hasVenue = (f.venueType === 1 && f.classRoomId) || (f.venueType === 2 && f.siteId)
|
||||
if (f.tId && f.startTime && f.endTime && hasVenue) {
|
||||
this.performConflictCheck(false)
|
||||
}
|
||||
},
|
||||
handleManualCheckConflict() {
|
||||
this.performConflictCheck(true)
|
||||
},
|
||||
performConflictCheck(showNoticeOnPass) {
|
||||
const f = this.lessonForm
|
||||
if (!f.startTime || !f.endTime) {
|
||||
if (showNoticeOnPass) this.$modal.msgWarning('请先选择课次开始与结束时间')
|
||||
return
|
||||
}
|
||||
if (f.startTime >= f.endTime) {
|
||||
if (showNoticeOnPass) this.$modal.msgError('课次结束时间必须晚于开始时间')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
planId: this.planId,
|
||||
lessonId: f.lessonId,
|
||||
planCourseId: f.planCourseId,
|
||||
tId: f.tId,
|
||||
startTime: f.startTime,
|
||||
endTime: f.endTime,
|
||||
venueType: f.venueType,
|
||||
classRoomId: f.venueType === 1 ? f.classRoomId : null,
|
||||
siteId: f.venueType === 2 ? f.siteId : null
|
||||
}
|
||||
this.conflictChecking = true
|
||||
checkLessonConflict(payload)
|
||||
.then(res => {
|
||||
this.conflictResult = res.data || { hasConflict: false }
|
||||
if (showNoticeOnPass && !this.conflictResult.hasConflict) {
|
||||
this.$modal.msgSuccess('预检通过,未检测到任何冲突')
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.conflictChecking = false
|
||||
})
|
||||
},
|
||||
// ---- 提交保存课次 (排课冲突告警确认) ----
|
||||
submitLessonForm() {
|
||||
this.$refs.lessonForm.validate(valid => {
|
||||
if (!valid) return
|
||||
if (this.lessonForm.startTime >= this.lessonForm.endTime) {
|
||||
this.$modal.msgError('课次结束时间必须晚于开始时间')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建保存 payload (严格对齐 LessonCreateDTO / LessonUpdateDTO,绝不含 force/override 字段)
|
||||
const payload = {
|
||||
planCourseId: this.lessonForm.planCourseId,
|
||||
tId: this.lessonForm.tId,
|
||||
startTime: this.lessonForm.startTime,
|
||||
endTime: this.lessonForm.endTime,
|
||||
venueType: this.lessonForm.venueType,
|
||||
classRoomId: this.lessonForm.venueType === 1 ? this.lessonForm.classRoomId : null,
|
||||
siteId: this.lessonForm.venueType === 2 ? this.lessonForm.siteId : null,
|
||||
remark: this.lessonForm.remark
|
||||
}
|
||||
|
||||
// 构建预检 DTO (编辑模式传 lessonId 排除自身,新增模式传 null)
|
||||
const conflictCheckDTO = {
|
||||
planId: this.planId,
|
||||
lessonId: this.dialog.isEdit ? this.lessonForm.lessonId : null,
|
||||
planCourseId: this.lessonForm.planCourseId,
|
||||
tId: this.lessonForm.tId,
|
||||
startTime: this.lessonForm.startTime,
|
||||
endTime: this.lessonForm.endTime,
|
||||
venueType: this.lessonForm.venueType,
|
||||
classRoomId: this.lessonForm.venueType === 1 ? this.lessonForm.classRoomId : null,
|
||||
siteId: this.lessonForm.venueType === 2 ? this.lessonForm.siteId : null
|
||||
}
|
||||
|
||||
this.dialog.submitting = true
|
||||
// 每次点击保存均重新发起冲突预检,避免依赖过期结果
|
||||
checkLessonConflict(conflictCheckDTO)
|
||||
.then(res => {
|
||||
const checkData = res.data || { hasConflict: false }
|
||||
this.conflictResult = checkData
|
||||
|
||||
if (checkData.hasConflict) {
|
||||
const warningMsg = checkData.conflictMessage || this.conflictTypeLabel(checkData.conflictType)
|
||||
this.$confirm(`检测到排课冲突:\n${warningMsg}\n\n是否仍然保存该课次?`, '排课冲突告警确认', {
|
||||
confirmButtonText: '确认保存',
|
||||
cancelButtonText: '返回调整',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.executeSaveLesson(payload)
|
||||
}).catch(() => {
|
||||
this.dialog.submitting = false
|
||||
})
|
||||
} else {
|
||||
this.executeSaveLesson(payload)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$modal.msgError('排课冲突检查失败,请稍后重试')
|
||||
this.dialog.submitting = false
|
||||
})
|
||||
})
|
||||
},
|
||||
executeSaveLesson(payload) {
|
||||
this.dialog.submitting = true
|
||||
const request = this.dialog.isEdit
|
||||
? updateLesson(this.lessonForm.lessonId, payload)
|
||||
: addLesson(this.planId, payload)
|
||||
|
||||
request
|
||||
.then(() => {
|
||||
this.$modal.msgSuccess(this.dialog.isEdit ? '修改课次成功' : '新增课次成功')
|
||||
this.dialog.open = false
|
||||
this.loadTimetable()
|
||||
})
|
||||
.finally(() => {
|
||||
this.dialog.submitting = false
|
||||
})
|
||||
},
|
||||
// ---- 删除课次 ----
|
||||
handleRemoveLesson(row) {
|
||||
this.$modal.confirm(`确定要删除课次【${row.courseName} (${row.startTime})】吗?`).then(() => {
|
||||
return removeLesson(row.lessonId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.loadTimetable()
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.timetable-panel {
|
||||
padding: 5px 0;
|
||||
}
|
||||
.empty-tip {
|
||||
padding: 40px 0;
|
||||
}
|
||||
.day-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.day-card {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.day-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.lesson-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.lesson-item-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #409EFF;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.lesson-time {
|
||||
width: 140px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.lesson-main {
|
||||
flex-grow: 1;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.lesson-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2f3d;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.lesson-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
.meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.lesson-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -69,9 +69,24 @@
|
||||
>{{ scope.row.courseCount || 0 }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参训人数" align="center" prop="enrolledCount" width="90" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="230" fixed="right">
|
||||
<el-table-column label="参训人数" align="center" prop="enrolledCount" width="90">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:enrollment:list']"
|
||||
type="text"
|
||||
@click="handleOpenPlanDetail(scope.row, 'enrollment')"
|
||||
>{{ scope.row.enrolledCount || 0 }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="290" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:query']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-data-board"
|
||||
@click="handleOpenPlanDetail(scope.row, 'course')"
|
||||
>计划详情</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:query']"
|
||||
size="mini"
|
||||
@@ -255,6 +270,93 @@
|
||||
<el-button @click="selectorDialog.open = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 计划详情综合对话框 (包含:课程、参训人员、课程表) -->
|
||||
<el-dialog
|
||||
:title="`培训计划详情 - ${planDetailDialog.plan.planName || ''}`"
|
||||
:visible.sync="planDetailDialog.open"
|
||||
width="1050px"
|
||||
append-to-body
|
||||
top="5vh"
|
||||
>
|
||||
<!-- 计划基础信息概览 -->
|
||||
<el-descriptions :column="3" border size="small" style="margin-bottom: 15px;">
|
||||
<el-descriptions-item label="计划编号">{{ planDetailDialog.plan.planId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="培训班名称">{{ planDetailDialog.plan.planName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag size="mini" :type="statusTagType(planDetailDialog.plan.planStatus)">
|
||||
{{ statusLabel(planDetailDialog.plan.planStatus) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="开班时间">{{ planDetailDialog.plan.startTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结业时间">{{ planDetailDialog.plan.endTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报到截止">{{ planDetailDialog.plan.reportDeadline }}</el-descriptions-item>
|
||||
<el-descriptions-item label="培训地点" :span="2">{{ planDetailDialog.plan.trainAddress || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划人数">{{ planDetailDialog.plan.recruitNum }} 人</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 核心三要素 Tab 切换 -->
|
||||
<el-tabs v-model="planDetailDialog.activeTab">
|
||||
<!-- Tab 1: 课程 -->
|
||||
<el-tab-pane label="本期课程" name="course">
|
||||
<div style="margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-weight: bold; font-size: 14px;">已配置课程 ({{ courseDialog.list.length }} 门)</span>
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:edit']"
|
||||
type="primary"
|
||||
size="mini"
|
||||
icon="el-icon-plus"
|
||||
:disabled="isLocked(planDetailDialog.plan.planStatus)"
|
||||
@click="handleOpenCourseSelector"
|
||||
>引入课程</el-button>
|
||||
</div>
|
||||
<el-table v-loading="courseDialog.loading" :data="courseDialog.list" border style="width: 100%">
|
||||
<el-table-column label="序号" type="index" width="55" align="center" />
|
||||
<el-table-column label="课程编码" prop="cCode" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="课程名称" prop="cName" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="类型" prop="cType" width="80" align="center" />
|
||||
<el-table-column label="学时" prop="cDuration" width="70" align="center" />
|
||||
<el-table-column label="排序号" prop="sortNo" width="70" align="center" />
|
||||
<el-table-column label="配置说明" prop="remark" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" width="80" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:edit']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
style="color: #F56C6C;"
|
||||
:disabled="isLocked(planDetailDialog.plan.planStatus)"
|
||||
@click="handleRemoveCourse(scope.row)"
|
||||
>移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 2: 参训人员 -->
|
||||
<el-tab-pane label="参训人员" name="enrollment">
|
||||
<enrollment-panel
|
||||
v-if="planDetailDialog.open && planDetailDialog.plan.planId"
|
||||
:plan-id="planDetailDialog.plan.planId"
|
||||
:plan-status="planDetailDialog.plan.planStatus"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 3: 课程表 -->
|
||||
<el-tab-pane label="课程表" name="timetable">
|
||||
<timetable-panel
|
||||
v-if="planDetailDialog.open && planDetailDialog.plan.planId"
|
||||
:plan-id="planDetailDialog.plan.planId"
|
||||
:plan-status="planDetailDialog.plan.planStatus"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="planDetailDialog.open = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -262,9 +364,15 @@
|
||||
import { addPlan, completePlan, getPlan, listPlan, updatePlan } from '@/api/train/plan'
|
||||
import { listPlanCourses, configurePlanCourses, removePlanCourse } from '@/api/train/planCourse'
|
||||
import { listCourse } from '@/api/train/course'
|
||||
import EnrollmentPanel from './components/EnrollmentPanel.vue'
|
||||
import TimetablePanel from './components/TimetablePanel.vue'
|
||||
|
||||
export default {
|
||||
name: 'Plan',
|
||||
components: {
|
||||
EnrollmentPanel,
|
||||
TimetablePanel
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
@@ -317,7 +425,13 @@ export default {
|
||||
},
|
||||
list: []
|
||||
},
|
||||
selectedCourses: []
|
||||
selectedCourses: [],
|
||||
// 计划综合详情对话框
|
||||
planDetailDialog: {
|
||||
open: false,
|
||||
activeTab: 'course',
|
||||
plan: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -503,6 +617,15 @@ export default {
|
||||
this.loadPlanCourses(this.courseDialog.planId)
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleOpenPlanDetail(row, activeTab = 'course') {
|
||||
this.planDetailDialog.plan = row
|
||||
this.planDetailDialog.activeTab = activeTab
|
||||
this.planDetailDialog.open = true
|
||||
this.courseDialog.planId = row.planId
|
||||
this.courseDialog.planName = row.planName
|
||||
this.courseDialog.planStatus = row.planStatus
|
||||
this.loadPlanCourses(row.planId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user