Files
education/frontend/src/views/teachBusiness/syllabus/index.vue
T
2026-08-25 19:25:55 +08:00

590 lines
20 KiB
Vue

<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
type="danger"
plain
icon="el-icon-delete"
:disabled="!selection.length"
@click="handleBatchDelete"
>删除所选</el-button>
<el-button icon="el-icon-download" @click="handleDownload">下载</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 prop="bh" label="编号" width="170" show-overflow-tooltip 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="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 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">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" 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>
</div>
</template>
<script>
import {
addSyllabus,
deleteSyllabus,
batchDeleteSyllabus,
updateSyllabus,
listSyllabus,
listSyllabusByZydhAndTy
} 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'
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,
selection: [],
referenceDataLoading: false,
majorOptions: [],
subjectOptions: [],
courseTypeOptions: [],
officeOptions: [],
dialog: {
visible: false,
title: '',
submitting: false,
form: this.createEmptyForm()
},
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() {
const start = (this.pageNum - 1) * this.pageSize
return this.tableData.slice(start, start + this.pageSize)
}
},
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
const { zydh, ty } = this.searchForm
// 专业代号存在时走后端组合查询;否则查全部后按停用标识过滤
const useApiQuery = Boolean(zydh)
const req = useApiQuery
? listSyllabusByZydhAndTy(zydh, ty)
: listSyllabus()
req.then(response => {
let list = response.data || []
if (!useApiQuery) {
if (zydh) list = list.filter(i => i.zydh && i.zydh.indexOf(zydh) !== -1)
list = list.filter(i => Number(i.ty) === Number(ty))
}
this.tableData = list
this.total = list.length
this.pageNum = 1
}).catch(() => {
this.tableData = []
this.total = 0
}).finally(() => {
this.loading = false
})
},
handleQuery() {
this.fetchList()
},
handleReset() {
this.searchForm = { zydh: '', ty: 0 }
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
this.fetchList()
}).finally(() => {
this.dialog.submitting = false
})
})
},
/* ---------- 删除 / 批量删除 ---------- */
handleDelete(row) {
this.$confirm('删除后该条记录将置为「停用」,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return deleteSyllabus(row.bh)
}).then(() => {
this.$message.success('删除成功')
this.fetchList()
}).catch(() => {})
},
handleBatchDelete() {
if (!this.selection.length) {
this.$message.warning('请先勾选要删除的记录')
return
}
const bhList = this.selection.map(row => row.bh)
this.$confirm('确定删除所选 ' + bhList.length + ' 条记录吗?(将置为「停用」)', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return batchDeleteSyllabus(bhList)
}).then(() => {
this.$message.success('批量删除成功')
this.fetchList()
}).catch(() => {})
},
/* ---------- 下载(无后端接口) ---------- */
handleDownload() {
this.$message.warning('后端暂未提供该接口')
},
/* ---------- 分页 ---------- */
handleSizeChange(size) {
this.pageSize = size
this.pageNum = 1
},
handlePageChange(page) {
this.pageNum = page
},
/* ---------- 工具 ---------- */
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;
}
}
</style>