Files
education/frontend/src/views/teachOffice/elective/index.vue
T
2026-09-22 16:25:55 +08:00

667 lines
21 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 management-page elective-page">
<!-- ==================== 2. 查询条件区域 ==================== -->
<div class="section-card query-card">
<div class="section-heading">
<div>
<h2 class="section-title">筛选条件</h2>
<p class="section-description">按课程班次教员及状态筛选选修课</p>
</div>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
</div>
<el-form>
<el-row :gutter="32">
<!-- 左栏 -->
<el-col :xs="24" :md="12">
<div class="query-field">
<span class="q-label no-check">授课教员</span>
<el-input v-model="searchForm.js" placeholder="请输入授课教员" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">可选班次年级</span>
<el-input v-model="searchForm.kbcnj" placeholder="请输入可选班次年级" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">选修课状态</span>
<el-select
v-model="searchForm.xxkzt"
placeholder="请选择选修课状态"
clearable
class="q-control"
>
<el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
</el-select>
</div>
</el-col>
<!-- 右栏 -->
<el-col :xs="24" :md="12">
<div class="query-field">
<span class="q-label no-check">课程科目名称</span>
<el-input v-model="searchForm.kckmmc" placeholder="请输入课程科目名称" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">选修班名称</span>
<el-input v-model="searchForm.xxbmc" placeholder="请输入选修班名称" clearable class="q-control" />
</div>
<div class="query-field">
<span class="q-label no-check">培训层次</span>
<el-select v-model="searchForm.pxcc" placeholder="请选择培训层次" clearable class="q-control">
<el-option v-for="item in pxccOptions" :key="item" :label="item" :value="item" />
</el-select>
</div>
</el-col>
</el-row>
</el-form>
</div>
<!-- ==================== 6. 数据表格区域 ==================== -->
<div class="section-card table-card">
<div class="list-header">
<div class="list-title">选修课列表<span class="list-sub"> {{ total }} 条数据</span></div>
<div class="list-actions">
<el-button type="primary" icon="el-icon-plus" @click="handleOpenNew">新建选修课</el-button>
<el-button type="primary" plain icon="el-icon-circle-check" @click="handleOpenEarly">
所选批量开放报名
</el-button>
<el-button type="warning" plain icon="el-icon-remove-outline" @click="handleCancelEarly">
所选批量取消开放报名
</el-button>
<el-button type="danger" plain icon="el-icon-circle-close" @click="handleStopSignup">
批量停止报名
</el-button>
<el-button icon="el-icon-download" @click="handleDownloadList">下载选修班列表</el-button>
<el-button icon="el-icon-download" @click="handleDownloadWord">学员名单Word</el-button>
<el-button icon="el-icon-download" @click="handleDownloadExcel">学员名单Excel</el-button>
<el-button icon="el-icon-folder-opened" @click="handleSelectFile">选择文件</el-button>
<span class="file-name" :class="{ 'has-file': selectedFile }">{{ fileName }}</span>
<input ref="fileInputRef" type="file" style="display: none" @change="handleFileChange" />
<el-button type="primary" plain icon="el-icon-upload2" @click="handleUpload">上传数据</el-button>
</div>
</div>
<el-table v-loading="loading" :data="tableData" border stripe class="main-table" @selection-change="handleSelectionChange">
<template slot="empty">
<span>无数据</span>
</template>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" :index="tableIndex" />
<el-table-column prop="kmc" label="课程" min-width="110" show-overflow-tooltip />
<el-table-column prop="jcmc" label="教材" min-width="120" show-overflow-tooltip />
<el-table-column prop="jyxm" label="实施教员" width="90" align="center" />
<el-table-column prop="jsmc" label="教学场地" width="100" show-overflow-tooltip />
<el-table-column prop="kxdlbc" label="显示可选队别班次" min-width="150" align="center" show-overflow-tooltip />
<el-table-column label="计划学时/运行学时/学分" width="130" align="center">
<template slot="header">
<div class="triple-header">
<span>计划学时</span>
<span>运行学时</span>
<span>学分</span>
</div>
</template>
<template slot-scope="{ row }">
<div class="triple-cell">
<span>{{ row.jhyxxsxf }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="开课结课日期" min-width="150" align="center">
<template slot-scope="{ row }">
{{ row.kkjkrq }}
</template>
</el-table-column>
<el-table-column label="报名日期" min-width="140" align="center">
<template slot-scope="{ row }">
{{ row.bmrq }}
</template>
</el-table-column>
<el-table-column prop="jhrs" label="计划人数" width="90" align="center" />
<el-table-column prop="yqsm" label="要求说明" min-width="120" show-overflow-tooltip />
</el-table>
<div class="pagination-wrap">
<el-pagination
background
layout="total, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 新建选修课弹窗 -->
<el-dialog
:visible="dialogVisible"
title="新建选修课"
width="560px"
@update:visible="val => (dialogVisible = val)"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="选修班名称" prop="xxbmc">
<el-input v-model="form.xxbmc" placeholder="请输入选修班名称" clearable />
</el-form-item>
<el-form-item label="年级">
<el-input v-model="form.nj" placeholder="新建选修班时填写,如 2024" clearable />
</el-form-item>
<el-form-item label="课程名称" prop="kbh">
<el-select
v-model="form.kbh"
placeholder="请选择课程名称"
clearable
filterable
:loading="courseLoading"
class="w-full"
>
<el-option
v-for="c in courseOptions"
:key="c.kbh"
:label="(c.kmc || '-') + '' + c.kbh + ''"
:value="c.kbh"
/>
</el-select>
</el-form-item>
<el-form-item label="实施教员" prop="js">
<el-select
v-model="form.js"
placeholder="请选择实施教员"
clearable
filterable
:loading="teacherLoading"
class="w-full"
>
<el-option
v-for="t in teacherOptions"
:key="t.jybh"
:label="(t.jyxm || '-') + '' + t.jybh + ''"
:value="t.jybh"
/>
</el-select>
</el-form-item>
<el-form-item label="教材">
<el-select
v-model="form.jc"
placeholder="请选择教材"
clearable
filterable
allow-create
default-first-option
:loading="materialLoading"
class="w-full"
>
<el-option
v-for="m in materialOptions"
:key="m.bh || m.id"
:label="m.mc"
:value="m.mc"
/>
</el-select>
</el-form-item>
<el-form-item label="教学场地">
<el-select
v-model="form.jxcd"
placeholder="请选择教学场地"
clearable
filterable
:loading="classroomLoading"
class="w-full"
>
<el-option
v-for="r in classroomOptions"
:key="r.jsbh"
:label="r.jsmc ? r.jsbh + ' · ' + r.jsmc : r.jsbh"
:value="r.jsbh"
/>
</el-select>
</el-form-item>
<el-form-item label="计划学时">
<el-input-number v-model="form.jhsxs" :min="0" :precision="0" class="w-full" />
</el-form-item>
<el-form-item label="运行学时">
<el-input-number v-model="form.yxsxs" :min="0" :precision="0" class="w-full" />
</el-form-item>
<el-form-item label="学分">
<el-input-number v-model="form.xf" :min="0" :precision="1" class="w-full" />
</el-form-item>
<el-form-item label="计划人数">
<el-input-number v-model="form.jh" :min="0" :precision="0" class="w-full" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleNewConfirm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listElective,
addElective,
batchOpenElective,
batchCancelOpenElective,
batchStopElective,
exportElectiveList,
exportElectiveStudentsExcel,
exportElectiveStudentsWord
} from '@/api/teachOffice/elective'
import { listKb } from '@/api/teachOffice/kb'
import { listTeacher } from '@/api/teachOffice/teacher'
import { listAllClassroom } from '@/api/teachBusiness/classroom'
import { listTeachingMaterial } from '@/api/textbook/teachingMaterial'
import { saveAs } from 'file-saver'
export default {
name: 'Elective',
data() {
return {
// ==================== 下拉选项 ====================
statusOptions: ['拟制', '开放报名', '停止报名', '已开课'],
pxccOptions: ['无', '本科', '硕士', '博士'],
// ==================== 查询条件 ====================
searchForm: {
js: '',
kbcnj: '',
xxkzt: '',
kckmmc: '',
xxbmc: '',
pxcc: ''
},
// ==================== 数据表格 ====================
tableData: [],
total: 0,
loading: false,
selectedRows: [],
// ==================== 文件上传 ====================
selectedFile: null,
// ==================== 新建选修课弹窗 ====================
dialogVisible: false,
form: {
xxbmc: '',
nj: '',
kbh: '',
js: '',
jc: '',
jxcd: '',
jhsxs: 0,
yxsxs: 0,
xf: 0,
jh: 0
},
// ==================== 新建弹窗下拉选项 ====================
courseOptions: [],
courseLoading: false,
teacherOptions: [],
teacherLoading: false,
classroomOptions: [],
classroomLoading: false,
materialOptions: [],
materialLoading: false,
rules: {
xxbmc: [{ required: true, message: '请输入选修班名称', trigger: 'blur' }],
kbh: [{ required: true, message: '请选择课程名称', trigger: 'change' }],
js: [{ required: true, message: '请选择实施教员', trigger: 'change' }]
},
// ==================== 分页 ====================
currentPage: 1,
pageSize: 20
}
},
computed: {
fileName() {
return (this.selectedFile && this.selectedFile.name) || '未选择任何文件'
}
},
created() {
this.loadList()
},
methods: {
// 组装查询过滤条件(列表与导出共用,导出不含分页参数)
buildFilter() {
const q = {}
if (this.searchForm.js) q.jyxm = this.searchForm.js
if (this.searchForm.kbcnj) q.kxbcnj = this.searchForm.kbcnj
if (this.searchForm.kckmmc) q.kmc = this.searchForm.kckmmc
if (this.searchForm.xxbmc) q.xxbmc = this.searchForm.xxbmc
if (this.searchForm.xxkzt) q.xkzt = this.searchForm.xxkzt
if (this.searchForm.pxcc) q.pxcc = this.searchForm.pxcc
return q
},
// ==================== 查询 ====================
handleQuery() {
this.currentPage = 1
this.loadList()
},
loadList() {
this.loading = true
const params = Object.assign({ pageNum: this.currentPage, pageSize: this.pageSize }, this.buildFilter())
listElective(params)
.then(res => {
const data = (res && res.data) || {}
this.tableData = data.records || []
this.total = data.total || 0
})
.finally(() => {
this.loading = false
})
},
// ==================== 导出 ====================
handleDownloadList() {
exportElectiveList(this.buildFilter())
.then(blob => {
saveAs(blob, '选修班列表.xlsx')
this.$message.success('选修班列表已下载')
})
.catch(() => {})
},
handleDownloadWord() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
exportElectiveStudentsWord(bhList)
.then(blob => {
saveAs(blob, '选修班学员名单.docx')
this.$message.success('选修班学员名单Word已下载')
})
.catch(() => {})
},
handleDownloadExcel() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
exportElectiveStudentsExcel(bhList)
.then(blob => {
saveAs(blob, '选修班学员名单.xlsx')
this.$message.success('选修班学员名单Excel已下载')
})
.catch(() => {})
},
// ==================== 操作按钮 ====================
handleOpenEarly() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
batchOpenElective(bhList)
.then(() => {
this.$message.success('所选班已开放报名')
this.loadList()
})
.catch(() => {})
},
handleCancelEarly() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
batchCancelOpenElective(bhList)
.then(() => {
this.$message.success('所选班已取消开放报名')
this.loadList()
})
.catch(() => {})
},
handleStopSignup() {
const bhList = this.selectedRows.map(row => row.bh)
if (bhList.length === 0) {
this.$message.warning('请先选择选修班')
return
}
batchStopElective(bhList)
.then(() => {
this.$message.success('已批量停止报名')
this.loadList()
})
.catch(() => {})
},
handleReverseRange() {
this.$message.warning('后端暂未提供该接口')
},
// ==================== 文件上传 ====================
handleSelectFile() {
this.$refs.fileInputRef && this.$refs.fileInputRef.click()
},
handleFileChange(e) {
this.selectedFile = e.target.files[0] || null
},
handleUpload() {
if (!this.selectedFile) {
this.$message.warning('请先选择文件')
return
}
this.$message.warning('后端暂未提供该接口')
},
// ==================== 表格多选 ====================
handleSelectionChange(rows) {
this.selectedRows = rows
},
// ==================== 新建选修课弹窗 ====================
handleOpenNew() {
this.form = {
xxbmc: '',
nj: '',
kbh: '',
js: '',
jc: '',
jxcd: '',
jhsxs: 0,
yxsxs: 0,
xf: 0,
jh: 0
}
this.dialogVisible = true
this.loadCourseOptions()
this.loadTeacherOptions()
this.loadClassroomOptions()
this.loadMaterialOptions()
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
loadCourseOptions() {
if (this.courseOptions.length || this.courseLoading) return
this.courseLoading = true
listKb({ pageNum: 1, pageSize: 500 }).then(res => {
this.courseOptions = (res && res.data && res.data.records) || []
}).catch(() => {
this.courseOptions = []
}).finally(() => {
this.courseLoading = false
})
},
loadTeacherOptions() {
if (this.teacherOptions.length || this.teacherLoading) return
this.teacherLoading = true
listTeacher({ pageNum: 1, pageSize: 500 }).then(res => {
const records = (res && res.data && res.data.records) || []
this.teacherOptions = records.filter(t => t.jybh && !t.lzzt)
}).catch(() => {
this.teacherOptions = []
}).finally(() => {
this.teacherLoading = false
})
},
loadClassroomOptions() {
if (this.classroomOptions.length || this.classroomLoading) return
this.classroomLoading = true
listAllClassroom().then(res => {
this.classroomOptions = (res && res.data ? res.data : [])
.filter(r => r.jsbh && !(r.ty === true || r.ty === 1 || r.ty === '1' || r.ty === 'true'))
.sort((a, b) => String(a.jsbh).localeCompare(String(b.jsbh), 'zh-CN'))
}).catch(() => {
this.classroomOptions = []
}).finally(() => {
this.classroomLoading = false
})
},
loadMaterialOptions() {
if (this.materialOptions.length || this.materialLoading) return
this.materialLoading = true
listTeachingMaterial({ pageNum: 1, pageSize: 500 }).then(res => {
const records = (res && res.data && res.data.records) || []
this.materialOptions = records.filter(m => m.mc && !(m.ty === true || m.ty === 1 || m.ty === '1' || m.ty === 'true'))
}).catch(() => {
this.materialOptions = []
}).finally(() => {
this.materialLoading = false
})
},
handleNewConfirm() {
if (!this.$refs.formRef) return
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.submitNewElective()
})
},
submitNewElective() {
const dto = {
kbh: this.form.kbh,
xxbmc: this.form.xxbmc,
nj: this.form.nj || undefined,
jybh: this.form.js || undefined,
jsbh: this.form.jxcd || undefined,
jcmc: this.form.jc || undefined,
rs: this.form.jh || undefined,
xs: this.form.jhsxs || undefined,
xf: this.form.xf || undefined
}
addElective(dto).then(() => {
this.$message.success('新建选修课成功')
this.dialogVisible = false
this.loadList()
}).catch(() => {})
},
// ==================== 分页 ====================
handlePageChange(page) {
this.currentPage = page
this.loadList()
},
// 表格序号(跨页连续)
tableIndex(index) {
return (this.currentPage - 1) * this.pageSize + index + 1
}
}
}
</script>
<style scoped lang="scss">
// ==================== 2. 查询条件区域 ====================
.query-card {
.query-field {
display: flex;
align-items: center;
margin-bottom: 14px;
min-height: 32px;
.q-label {
width: 120px;
flex-shrink: 0;
font-size: 13px;
color: #303133;
text-align: right;
margin-right: 10px;
white-space: nowrap;
}
.q-control {
flex: 1;
min-width: 0;
}
}
}
.w-full {
width: 100%;
}
// ==================== 6. 数据表格区域 ====================
.table-card {
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
.list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
.list-sub {
margin-left: 10px;
font-size: 13px;
font-weight: 400;
color: #909399;
}
}
.list-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
.el-button + .el-button {
margin-left: 0;
}
.file-name {
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: #999;
&.has-file {
color: #303133;
}
}
}
}
.main-table {
::v-deep .cell {
font-size: 12px;
}
}
.triple-header,
.triple-cell {
display: flex;
flex-direction: column;
line-height: 1.4;
}
.pagination-wrap {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
</style>