Files
education/frontend/src/views/teachOffice/elective/index.vue
T
2026-08-26 14:12:00 +08:00

610 lines
20 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>
<!-- ==================== 3. 导出按钮区域 ==================== -->
<div class="section-card export-card">
<div class="section-heading section-heading--compact">
<div>
<h2 class="section-title">数据导出</h2>
<p class="section-description">下载选修班及学员名单</p>
</div>
</div>
<div class="export-bar">
<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>
</div>
</div>
<!-- ==================== 4. 操作按钮区域 ==================== -->
<div class="section-card action-card">
<div class="section-heading section-heading--compact">
<div>
<h2 class="section-title">选修课操作</h2>
<p class="section-description">新建选修课或对已选数据执行批量操作</p>
</div>
</div>
<div class="action-row">
<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-refresh" @click="handleReverseRange">
所选批量根据学员反向确定班次范围
</el-button>
</div>
<div class="action-row">
<el-button icon="el-icon-download" @click="handleDownloadTemplate">选修课数据文件模板下载</el-button>
</div>
</div>
<!-- ==================== 5. 文件上传区域 ==================== -->
<div class="section-card upload-card">
<div class="section-heading section-heading--compact">
<div>
<h2 class="section-title">数据导入</h2>
<p class="section-description">选择填写完成的模板文件并上传</p>
</div>
</div>
<div class="upload-row">
<div class="upload-left">
<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" />
</div>
<div class="upload-right">
<el-button type="primary" icon="el-icon-upload2" @click="handleUpload">上传数据</el-button>
</div>
</div>
</div>
<!-- ==================== 6. 数据表格区域 ==================== -->
<div class="section-card table-card">
<div class="section-heading section-heading--compact table-heading">
<div>
<h2 class="section-title">选修课列表</h2>
<p class="section-description"> {{ total }} 条数据</p>
</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="kcmc">
<el-input v-model="form.kcmc" placeholder="请输入课程名称,提交时按名称查找课编号" clearable />
</el-form-item>
<el-form-item label="实施教员" prop="js">
<el-input v-model="form.js" placeholder="请输入实施教员,提交时按名称查找教员编号" clearable />
</el-form-item>
<el-form-item label="教材">
<el-input v-model="form.jc" placeholder="请输入教材" clearable />
</el-form-item>
<el-form-item label="教学场地">
<el-input v-model="form.jxcd" placeholder="请输入教学场地,提交时按名称查找教室编号" clearable />
</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 { listClassroom } from '@/api/teachBusiness/classroom'
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: '',
kcmc: '',
js: '',
jc: '',
jxcd: '',
jhsxs: 0,
yxsxs: 0,
xf: 0,
jh: 0
},
rules: {
xxbmc: [{ required: true, message: '请输入选修班名称', trigger: 'blur' }],
kcmc: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
js: [{ required: true, message: '请输入实施教员', trigger: 'blur' }]
},
// ==================== 分页 ====================
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('后端暂未提供该接口')
},
handleDownloadTemplate() {
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: '',
kcmc: '',
js: '',
jc: '',
jxcd: '',
jhsxs: 0,
yxsxs: 0,
xf: 0,
jh: 0
}
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
},
handleNewConfirm() {
if (!this.$refs.formRef) return
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.submitNewElective()
})
},
submitNewElective() {
const emptyPage = { data: { records: [] } }
Promise.all([
listKb({ pageNum: 1, pageSize: 1, kmc: this.form.kcmc }),
listTeacher({ pageNum: 1, pageSize: 1, jyxm: this.form.js }),
this.form.jxcd ? listClassroom({ pageNum: 1, pageSize: 1, jsmc: this.form.jxcd }) : Promise.resolve(emptyPage)
])
.then(([kbRes, teacherRes, roomRes]) => {
const kbh = kbRes?.data?.records?.[0]?.kbh
const jybh = teacherRes?.data?.records?.[0]?.jybh
const jsbh = roomRes?.data?.records?.[0]?.jsbh
if (!kbh) {
this.$message.warning(`未找到课程科目:${this.form.kcmc}`)
return
}
const dto = {
kbh: kbh,
xxbmc: this.form.xxbmc,
nj: this.form.nj || undefined,
jybh: jybh || undefined,
jsbh: jsbh || undefined,
rs: this.form.jh || undefined,
xs: this.form.jhsxs || undefined,
xf: this.form.xf || undefined
}
return 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;
}
}
}
.export-bar {
align-items: center;
}
// ==================== 4. 操作按钮区域 ====================
.action-card {
.action-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
}
// ==================== 5. 文件上传区域 ====================
.upload-card {
.upload-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
.upload-left {
display: flex;
align-items: center;
gap: 12px;
.file-name {
font-size: 12px;
color: #999;
&.has-file {
color: #303133;
}
}
}
.upload-right {
display: flex;
align-items: center;
}
}
}
// ==================== 6. 数据表格区域 ====================
.table-card {
.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>