Files
education/frontend/src/views/teachBusiness/syllabus/index.vue
T

808 lines
27 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 syllabus-page">
<!-- 查询条件 -->
<el-card shadow="never" class="search-card">
<el-form :model="searchForm" label-width="100px" class="search-form" @submit.native.prevent>
<el-row :gutter="32">
<el-col :xs="24" :md="8">
<el-form-item label="专业代号">
<el-input v-model="searchForm.zydh" placeholder="请输入专业代号" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="停用">
<el-radio-group v-model="searchForm.ty">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<div class="search-actions">
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 数据列表 -->
<el-card shadow="never" class="table-card">
<div class="list-header">
<div class="list-title">教学大纲列表</div>
<div class="list-actions">
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
<el-button
v-if="Number(searchForm.ty) !== 1"
type="danger"
plain
icon="el-icon-circle-close"
:disabled="!activeSelection.length"
@click="handleBatchDelete"
>停用所选</el-button>
<el-button icon="el-icon-download" @click="handleDownload">下载</el-button>
<el-button icon="el-icon-download" @click="handleDownloadTemplate">下载模板</el-button>
<el-button icon="el-icon-upload2" @click="openImportDialog">导入</el-button>
</div>
</div>
<el-table v-loading="loading" :data="pagedData" border stripe highlight-current-row
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column type="index" label="序号" width="50" align="center" />
<el-table-column prop="zydh" label="专业代号" width="150" align="center" />
<el-table-column prop="kbh" label="课编号" width="110" show-overflow-tooltip align="center" />
<el-table-column prop="jc" label="简称" width="100" show-overflow-tooltip align="center" />
<el-table-column prop="kcdw" label="课程定位" width="120" show-overflow-tooltip align="center" />
<el-table-column prop="mk" label="模块" width="100" show-overflow-tooltip align="center" />
<el-table-column prop="jysdh" label="教研室" width="100" show-overflow-tooltip align="center" />
<el-table-column prop="klx" label="课类型" width="80" align="center" />
<el-table-column prop="xqdc" label="学期第次" width="80" align="center" />
<el-table-column prop="xs" label="学时" width="70" align="center" />
<el-table-column prop="xf" label="学分" width="70" align="center" />
<el-table-column prop="llxs" label="理论学时" width="80" align="center" />
<el-table-column prop="sjxs" label="实践学时" width="80" align="center" />
<el-table-column prop="zks" label="周课时" width="70" align="center" />
<el-table-column prop="ksks" label="考试课时" width="80" align="center" />
<el-table-column prop="cjfz" label="成绩分制" width="90" align="center" />
<el-table-column label="不计入平均分" width="110" align="center">
<template slot-scope="scope">{{ fmtYesNo(scope.row.bjrxypjf) }}</template>
</el-table-column>
<el-table-column label="考试课时不显示" width="130" align="center">
<template slot-scope="scope">{{ fmtYesNo(scope.row.ksksbxs) }}</template>
</el-table-column>
<el-table-column label="大纲课程" width="90" align="center">
<template slot-scope="scope">{{ fmtYesNo(scope.row.dgkc) }}</template>
</el-table-column>
<el-table-column label="停用" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="Number(scope.row.ty) === 1 ? 'danger' : 'success'" size="mini">
{{ Number(scope.row.ty) === 1 ? '停用' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="启用时间" width="150" align="center">
<template slot-scope="scope">{{ fmtDateTime(scope.row.qysj) }}</template>
</el-table-column>
<el-table-column label="停用时间" width="150" align="center">
<template slot-scope="scope">{{ fmtDateTime(scope.row.tysj) }}</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" fixed="right" class-name="table-action-column">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button
v-if="!isDisabled(scope.row)"
type="text"
size="mini"
icon="el-icon-circle-close"
class="danger-text-btn"
@click="handleDelete(scope.row)"
>停用</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
background
layout="total, sizes, prev, pager, next"
:current-page="pageNum"
:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="dialog.title" :visible.sync="dialog.visible" width="720px" append-to-body
:close-on-click-modal="false">
<el-form ref="syllabusForm" :model="dialog.form" :rules="rules" label-width="130px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="专业" prop="zydh">
<el-select
v-model="dialog.form.zydh"
filterable
clearable
placeholder="请选择专业"
class="w-full"
:loading="referenceDataLoading"
>
<el-option
v-for="item in majorOptions"
:key="item.zydh"
:label="item.zymc"
:value="item.zydh"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课程" prop="kbh">
<el-select
v-model="dialog.form.kbh"
filterable
clearable
placeholder="请选择课程"
class="w-full"
:loading="referenceDataLoading"
>
<el-option
v-for="item in subjectOptions"
:key="item.kbh"
:label="item.kmc"
:value="item.kbh"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学期第次" prop="xqdc">
<el-input-number v-model="dialog.form.xqdc" :min="1" :max="20" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课类型" prop="klx">
<el-select
v-model="dialog.form.klx"
filterable
clearable
placeholder="请选择课类型"
class="w-full"
:loading="referenceDataLoading"
>
<el-option
v-for="item in courseTypeOptions"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学时" prop="xs">
<el-input-number v-model="dialog.form.xs" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="停用" prop="ty">
<el-radio-group v-model="dialog.form.ty">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="简称">
<el-input v-model="dialog.form.jc" placeholder="请输入课程简称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学分">
<el-input-number v-model="dialog.form.xf" :min="0" :step="0.5" :precision="1" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课程定位">
<el-input v-model="dialog.form.kcdw" placeholder="请输入课程定位" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模块">
<el-input v-model="dialog.form.mk" placeholder="请输入模块" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成绩分制">
<el-input v-model="dialog.form.cjfz" placeholder="请输入成绩分制" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教研室">
<el-select
v-model="dialog.form.jysdh"
filterable
clearable
placeholder="请选择教研室"
class="w-full"
:loading="referenceDataLoading"
>
<el-option
v-for="item in officeOptions"
:key="item.jysdh"
:label="item.jysmc"
:value="item.jysdh"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="考试课时">
<el-input-number v-model="dialog.form.ksks" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="理论学时">
<el-input-number v-model="dialog.form.llxs" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实践学时">
<el-input-number v-model="dialog.form.sjxs" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="周课时">
<el-input-number v-model="dialog.form.zks" :min="0" controls-position="right" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="不计入平均分">
<el-radio-group v-model="dialog.form.bjrxypjf">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="考试课时不显示">
<el-radio-group v-model="dialog.form.ksksbxs">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="大纲课程">
<el-radio-group v-model="dialog.form.dgkc">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialog.visible = false"> </el-button>
<el-button type="primary" :loading="dialog.submitting" @click="submitDialog"> </el-button>
</div>
</el-dialog>
<!-- 模板导入弹窗 -->
<el-dialog title="教学大纲导入" :visible.sync="importDialog.visible" width="660px" append-to-body
:close-on-click-modal="false">
<el-alert type="info" :closable="false" show-icon class="import-tip"
title="请先下载导入模板,按「专业教学计划」sheet 列填写后再上传;「专业代号+课编号+学期第次」已存在的行将跳过。" />
<el-form label-width="110px" class="import-form">
<el-form-item label="导入模板">
<el-button icon="el-icon-download" :loading="importDialog.downloading" @click="handleDownloadTemplate">
下载导入模板
</el-button>
<span class="tip-text">红色表头列为必填详见模板填写说明sheet</span>
</el-form-item>
<el-form-item label="数据文件">
<div class="file-input">
<input ref="importFileInput" type="file" accept=".xls,.xlsx" style="display: none"
@change="handleImportFileChange" />
<el-button icon="el-icon-folder-opened" @click="handleChooseFile">选择文件</el-button>
<span class="file-name">{{ importDialog.fileName || '未选择任何文件' }}</span>
<el-button v-if="importDialog.file" type="text" class="danger-text-btn" @click="clearImportFile">
清除
</el-button>
</div>
</el-form-item>
<el-form-item label="导入结果" v-if="importDialog.result">
<div class="import-result">
共导入 <span class="num">{{ importDialog.result.insertCount }}</span> <template
v-if="importDialog.result.skipCount > 0">跳过 <span
class="num warn">{{ importDialog.result.skipCount }}</span> 数据已存在</template><template
v-if="importDialog.result.failCount > 0">失败 <span
class="num warn">{{ importDialog.result.failCount }}</span> </template>
<div v-if="importDialog.result.failList.length" class="skip-detail">
<div v-for="(item, idx) in importDialog.result.failList" :key="'f' + idx">{{ item }}</div>
</div>
<div v-if="importDialog.result.skipList.length" class="skip-detail">
<div v-for="(item, idx) in importDialog.result.skipList" :key="'s' + idx">{{ item }}</div>
</div>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="importDialog.visible = false"> </el-button>
<el-button type="primary" :loading="importDialog.importing" :disabled="!importDialog.file"
@click="handleImport">开始导入</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
addSyllabus,
deleteSyllabus,
batchDeleteSyllabus,
updateSyllabus,
listByZydhAndTy,
exportSyllabus,
downloadSyllabusTemplate,
importSyllabus
} from '@/api/teachBusiness/syllabus'
import { listMajor } from '@/api/subjectMajor/major'
import { listKb } from '@/api/teachOffice/kb'
import { listOffice } from '@/api/teachOffice/office'
import { getDicts } from '@/api/system/dict/data'
import { saveAs } from 'file-saver'
const REFERENCE_DATA_PAGE_SIZE = 10000
const COURSE_TYPE_DICT_CODE = 'course_type'
export default {
name: 'SyllabusIndex',
data() {
return {
loading: false,
searchForm: {
zydh: '',
ty: 0
},
tableData: [],
total: 0,
pageNum: 1,
pageSize: 20,
localPaging: false,
selection: [],
referenceDataLoading: false,
majorOptions: [],
subjectOptions: [],
courseTypeOptions: [],
officeOptions: [],
dialog: {
visible: false,
title: '',
submitting: false,
form: this.createEmptyForm()
},
importDialog: {
visible: false,
downloading: false,
importing: false,
file: null,
fileName: '',
result: null
},
rules: {
zydh: [{ required: true, message: '请选择专业', trigger: 'change' }],
kbh: [{ required: true, message: '请选择课程', trigger: 'change' }],
xqdc: [{ required: true, message: '请输入学期第次', trigger: 'blur' }],
klx: [{ required: true, message: '请选择课类型', trigger: 'change' }],
xs: [{ required: true, message: '请输入学时', trigger: 'blur' }],
ty: [{ required: true, message: '请选择停用标识', trigger: 'change' }]
}
}
},
computed: {
pagedData() {
if (!this.localPaging) {
return this.tableData
}
const start = (this.pageNum - 1) * this.pageSize
return this.tableData.slice(start, start + this.pageSize)
},
activeSelection() {
return this.selection.filter(row => !this.isDisabled(row))
}
},
created() {
this.fetchList()
this.loadReferenceData()
},
methods: {
loadReferenceData() {
this.referenceDataLoading = true
const query = { pageNum: 1, pageSize: REFERENCE_DATA_PAGE_SIZE }
const requests = [
listMajor(query),
listKb(query),
listOffice(query),
getDicts(COURSE_TYPE_DICT_CODE)
].map(request => {
return request
.then(response => this.getResponseRecords(response))
.catch(() => [])
})
Promise.all(requests).then(([majors, subjects, offices, courseTypes]) => {
this.majorOptions = this.getUniqueOptions(majors, 'zydh', 'zymc')
this.subjectOptions = this.getUniqueOptions(subjects, 'kbh', 'kmc')
this.officeOptions = this.getUniqueOptions(offices, 'jysdh', 'jysmc')
this.courseTypeOptions = this.getUniqueOptions(courseTypes, 'dictValue', 'dictLabel')
}).finally(() => {
this.referenceDataLoading = false
})
},
getResponseRecords(response) {
const data = response.data || {}
return Array.isArray(data) ? data : data.records || []
},
getUniqueOptions(items, valueKey, labelKey) {
const values = new Set()
return items.filter(item => {
const value = item[valueKey]
const label = item[labelKey]
if (!value || !label || values.has(value)) {
return false
}
values.add(value)
return true
})
},
/* ---------- 列表加载 ---------- */
fetchList() {
this.loading = true
this.localPaging = false
const query = {
pageNum: this.pageNum,
pageSize: this.pageSize,
ty: this.searchForm.ty
}
if (this.searchForm.zydh) {
query.zydh = this.searchForm.zydh
}
listByZydhAndTy(query).then(response => {
const data = response.data || {}
this.tableData = Array.isArray(data) ? data : (data.records || [])
this.total = Array.isArray(data) ? data.length : Number(data.total || 0)
}).catch(() => {
this.tableData = []
this.total = 0
}).finally(() => {
this.loading = false
})
},
handleQuery() {
this.pageNum = 1
this.fetchList()
},
handleReset() {
this.searchForm = { zydh: '', ty: 0 }
this.pageNum = 1
this.fetchList()
},
handleSelectionChange(val) {
this.selection = val
},
/* ---------- 新增 / 编辑 ---------- */
handleAdd() {
this.dialog.title = '新增教学大纲'
this.dialog.form = this.createEmptyForm()
this.dialog.visible = true
this.$nextTick(() => {
if (this.$refs.syllabusForm) this.$refs.syllabusForm.clearValidate()
})
},
handleEdit(row) {
this.dialog.title = '编辑教学大纲'
this.dialog.form = Object.assign({}, this.createEmptyForm(), row)
this.dialog.visible = true
this.$nextTick(() => {
if (this.$refs.syllabusForm) this.$refs.syllabusForm.clearValidate()
})
},
submitDialog() {
this.$refs.syllabusForm.validate(valid => {
if (!valid) return
this.dialog.submitting = true
const payload = this.cleanPayload(this.dialog.form)
const isEdit = !!payload.bh
const req = isEdit ? updateSyllabus(payload) : addSyllabus(payload)
req.then(() => {
this.$message.success(isEdit ? '修改成功' : '新增成功')
this.dialog.visible = false
if (!isEdit) {
this.pageNum = 1
}
this.fetchList()
}).finally(() => {
this.dialog.submitting = false
})
})
},
/* ---------- 停用 / 批量停用 ---------- */
isDisabled(row) {
return Number(row && row.ty) === 1
},
handleDelete(row) {
if (this.isDisabled(row)) {
return
}
this.$confirm('确认停用该条教学大纲吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return deleteSyllabus(row.bh)
}).then(() => {
this.$message.success('停用成功')
this.fetchList()
}).catch(() => {})
},
handleBatchDelete() {
const bhList = this.activeSelection.map(row => row.bh)
if (!bhList.length) {
this.$message.warning('请先勾选要停用的记录')
return
}
this.$confirm('确定停用所选 ' + bhList.length + ' 条记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return batchDeleteSyllabus(bhList)
}).then(() => {
this.$message.success('批量停用成功')
this.fetchList()
}).catch(() => {})
},
/* ---------- 下载 ---------- */
handleDownload() {
const query = {}
if (this.searchForm.zydh) query.zydh = this.searchForm.zydh
if (this.searchForm.ty === 0 || this.searchForm.ty === 1) query.ty = this.searchForm.ty
exportSyllabus(query).then(blob => {
saveAs(blob, '教学大纲.xlsx')
this.$message.success('导出成功')
}).catch(() => {})
},
/* ---------- 模板导入 ---------- */
openImportDialog() {
this.importDialog.file = null
this.importDialog.fileName = ''
this.importDialog.result = null
this.importDialog.visible = true
if (this.$refs.importFileInput) this.$refs.importFileInput.value = ''
},
handleDownloadTemplate() {
this.importDialog.downloading = true
downloadSyllabusTemplate().then(blob => {
saveAs(blob, '教学大纲导入模板.xlsx')
this.$message.success('模板下载成功')
}).catch(() => {}).finally(() => {
this.importDialog.downloading = false
})
},
handleChooseFile() {
if (this.$refs.importFileInput) this.$refs.importFileInput.click()
},
handleImportFileChange(e) {
const input = e.target
this.importDialog.result = null
if (!input.files || input.files.length === 0) {
this.importDialog.file = null
this.importDialog.fileName = ''
return
}
const file = input.files[0]
const name = (file.name || '').toLowerCase()
if (!name.endsWith('.xls') && !name.endsWith('.xlsx')) {
this.$message.warning('仅支持 .xls / .xlsx 格式的 Excel 文件')
this.clearImportFile()
input.value = ''
return
}
this.importDialog.file = file
this.importDialog.fileName = file.name
},
clearImportFile() {
this.importDialog.file = null
this.importDialog.fileName = ''
this.importDialog.result = null
if (this.$refs.importFileInput) this.$refs.importFileInput.value = ''
},
handleImport() {
if (!this.importDialog.file) {
this.$message.warning('请先选择文件')
return
}
this.importDialog.importing = true
importSyllabus(this.importDialog.file).then(response => {
const data = (response && response.data) || {}
this.importDialog.file = null
this.importDialog.fileName = ''
if (this.$refs.importFileInput) this.$refs.importFileInput.value = ''
this.importDialog.result = {
insertCount: data.insertCount || 0,
skipCount: data.skipCount || 0,
failCount: data.failCount || 0,
skipList: data.skipList || [],
failList: data.failList || []
}
this.$message.success(data.message || '导入完成')
this.fetchList()
}).catch(() => {}).finally(() => {
this.importDialog.importing = false
})
},
/* ---------- 分页 ---------- */
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
this.fetchList()
},
handlePageChange(page) {
this.pageNum = page
this.fetchList()
},
/* ---------- 工具 ---------- */
createEmptyForm() {
return {
bh: '',
zydh: '',
kbh: '',
xqdc: 1,
klx: '',
xs: 0,
ty: 0,
jc: '',
xf: null,
kcdw: '',
ksks: 0,
mk: '',
cjfz: '',
bjrxypjf: 0,
llxs: 0,
sjxs: 0,
zks: 0,
ksksbxs: 0,
dgkc: 0,
jysdh: ''
}
},
/** 移除空值(''/null/undefined),保留 0 等有效值 */
cleanPayload(obj) {
const payload = {}
Object.keys(obj).forEach(key => {
const value = obj[key]
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
})
return payload
},
fmtYesNo(val) {
return Number(val) === 1 ? '是' : '否'
},
fmtDateTime(val) {
return (val || '').substring(0, 16) || '-'
}
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 20px;
}
.syllabus-page {
.search-card {
margin-bottom: 16px;
.search-form {
.w-full {
width: 100%;
}
.search-actions {
padding-top: 4px;
}
}
}
.table-card {
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.pagination {
margin-top: 16px;
text-align: right;
}
}
}
.danger-text-btn {
color: #f56c6c;
&:hover {
color: #f78989;
}
}
.import-tip {
margin-bottom: 16px;
}
.tip-text {
margin-left: 10px;
font-size: 12px;
color: #909399;
}
.file-input {
display: flex;
align-items: center;
.file-name {
margin-left: 10px;
font-size: 13px;
color: #606266;
}
}
.import-result {
font-size: 13px;
color: #303133;
.num {
font-weight: 600;
color: #409eff;
}
.num.warn {
color: #e6a23c;
}
.skip-detail {
margin-top: 8px;
max-height: 180px;
overflow-y: auto;
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
font-size: 12px;
color: #909399;
line-height: 1.8;
}
}
</style>