1、课时统计功能调整新增;

2、教学楼序号改为自动填充,无需填写;
3、教学楼检索,教学楼代号下拉选择;
4、课表冲突检查修复
This commit is contained in:
2026-09-24 15:07:26 +08:00
parent bd395a5504
commit f8e6466d24
5 changed files with 171 additions and 58 deletions
@@ -6,7 +6,10 @@
<el-row :gutter="24">
<el-col :xs="24" :md="8">
<el-form-item label="教学楼代号">
<el-input v-model="searchForm.jxldh" placeholder="请输入教学楼代号" clearable @keyup.enter.native="handleQuery" />
<el-select v-model="searchForm.jxldh" placeholder="请选择教学楼代号" clearable filterable
@change="handleQuery">
<el-option v-for="item in jxldhOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
@@ -71,9 +74,6 @@
<el-form-item label="地点">
<el-input v-model="dialog.form.dd" placeholder="请输入地点" maxlength="50" show-word-limit clearable />
</el-form-item>
<el-form-item label="序号">
<el-input v-model="dialog.form.xh" placeholder="留空则自动生成(按现有最大序号 +1" maxlength="50" clearable />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="dialog.form.bz" type="textarea" :rows="3" placeholder="请输入备注" maxlength="100"
show-word-limit />
@@ -150,6 +150,7 @@ export default {
dd: ''
},
tableData: [],
jxldhOptions: [],
selectedRows: [],
total: 0,
pageNum: 1,
@@ -183,6 +184,7 @@ export default {
},
created() {
this.fetchList()
this.loadJxldhOptions()
},
methods: {
/* ---------- 查询 ---------- */
@@ -218,6 +220,19 @@ export default {
})
},
/* ---------- 教学楼代号下拉(全量代号,独立于筛选条件) ---------- */
loadJxldhOptions() {
listBuild({ pageNum: 1, pageSize: 1000 }).then(response => {
const records = ((response && response.data) || {}).records || []
const seen = {}
this.jxldhOptions = records
.map(item => (item.jxldh || '').trim())
.filter(code => code && !seen[code] && (seen[code] = true))
}).catch(() => {
this.jxldhOptions = []
})
},
handleQuery() {
this.pageNum = 1
this.fetchList()
@@ -295,8 +310,9 @@ export default {
jxlmc: (form.jxlmc || '').trim()
}
if (this.dialog.isEdit) payload.id = form.id
// 可选字段留空时不提交,交由后端自动生成 / 保持原值
const optional = { dd: form.dd, xh: form.xh, bz: form.bz }
// 可选字段留空时不提交,交由后端自动生成 / 保持原值
// 序号 xh 不在表单中录入,新增由后端自动生成、修改保持原值
const optional = { dd: form.dd, bz: form.bz }
Object.keys(optional).forEach(key => {
const value = (optional[key] === null || optional[key] === undefined) ? '' : String(optional[key]).trim()
if (value !== '') payload[key] = value
@@ -317,6 +333,7 @@ export default {
this.dialog.submitting = false
this.dialog.visible = false
this.fetchList()
this.loadJxldhOptions()
}).catch(() => {
this.dialog.submitting = false
})
@@ -356,6 +373,7 @@ export default {
this.$message.success('已删除 ' + deleteCount + ' 条教学楼数据')
}
this.fetchList()
this.loadJxldhOptions()
}).catch(() => {
this.loading = false
this.fetchList()
@@ -432,6 +450,7 @@ export default {
const skipTip = data.skipCount > 0 ? ',跳过 ' + data.skipCount + ' 条已存在代号' : ''
this.$message.success('导入成功,共导入 ' + (data.insertCount || 0) + ' 条' + skipTip)
this.fetchList()
this.loadJxldhOptions()
}).catch(() => {
this.importDialog.importing = false
})
@@ -92,7 +92,7 @@
<div class="list-title">{{ pageTitle }}</div>
<div class="list-actions">
<el-button type="primary" plain size="mini" @click="handleExportPlan">学期教学实施计划</el-button>
<el-button type="primary" plain size="mini" @click="handleExportGrades">学期成绩表</el-button>
<el-button v-if="!isTeacher" type="primary" plain size="mini" @click="handleExportGrades">学期成绩表</el-button>
<el-button type="primary" plain size="mini" @click="handleExportCourses">课程列表另存Excel</el-button>
</div>
</div>
@@ -125,6 +125,7 @@
</template>
<script>
import { mapGetters } from 'vuex'
import { listToday, listWeek, listClass, exportClassPlan, exportClassGrades, exportClassCourses } from '@/api/teachBusiness/timetable'
import { listTeam } from '@/api/studentRecords/team'
@@ -167,6 +168,13 @@ export default {
}
},
computed: {
...mapGetters(['roles']),
// 与后端 JwglRoleHelper.isTeacher 口径一致:教员且非机关/教研室/管理员
isTeacher() {
const r = this.roles || []
const has = k => r.includes(k) || r.includes(k.toLowerCase())
return has('TEACHER') && !has('admin') && !has('DEPARTMENT_PERSONNEL') && !has('RESEARCH_OFFICE')
},
selectedTodayDate() {
const date = new Date(this.todayDate)
date.setDate(date.getDate() + this.dayOffset)
@@ -195,6 +203,10 @@ export default {
this.loadTeamOptions()
this.getTodayList()
this.getWeekList()
// 教员不依赖队别班次选择,直接加载本人课程
if (this.isTeacher) {
this.getShiftList()
}
},
methods: {
weekDayName(d) {
@@ -308,9 +320,11 @@ export default {
},
/* ---------- 班次:拉取数据 ---------- */
getShiftList() {
if (!this.searchForm.teamClass) return
const xydbh = this.teamMap[this.searchForm.teamClass] || this.searchForm.teamClass
const params = { xydbh: xydbh, pageNum: 1, pageSize: 1000 }
if (!this.searchForm.teamClass && !this.isTeacher) return
const params = { pageNum: 1, pageSize: 1000 }
if (this.searchForm.teamClass) {
params.xydbh = this.teamMap[this.searchForm.teamClass] || this.searchForm.teamClass
}
this.loading = true
listClass(params).then(response => {
const data = response.data || {}
@@ -323,7 +337,7 @@ export default {
},
/* ---------- 班次:查询 ---------- */
handleQuery() {
if (!this.searchForm.teamClass) {
if (!this.searchForm.teamClass && !this.isTeacher) {
this.$message.warning('请先选择队别班次')
return
}
@@ -340,7 +354,8 @@ export default {
window.URL.revokeObjectURL(link.href)
},
shiftQueryParams() {
if (!this.searchForm.teamClass) return null
// 教员可不选班次:后端会按本人教员编号限定
if (!this.searchForm.teamClass) return this.isTeacher ? {} : null
const xydbh = this.teamMap[this.searchForm.teamClass] || this.searchForm.teamClass
return { xydbh: xydbh }
},
@@ -117,14 +117,18 @@
import { checkConflict, checkAllConflict, resetConflict, getConflictDetails } from '@/api/teachBusiness/timetableConflict'
import { listAllSemester } from '@/api/teachBusiness/semester'
// 类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量;
// 类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量;
// 检查状态与冲突数完全来自后端接口)
// 注意:教员卡片含「教员历不可排」、教室卡片含「场地历不可用」,与排课窗口硬冲突口径一致
// 注意:教员卡片含「教员历不可排」、教室卡片含「场地历不可用」,与排课窗口硬冲突口径一致
// 保障资源、院历/班历事件两个维度也必须列出——后端 checkAll 会全部执行,缺卡片会导致
// 冲突总数计入消息却找不到对应卡片回显明细
const CARD_DEFS = [
{ key: 'TEAM_CONFLICT', label: '教学班时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程' },
{ key: 'ELECTIVE_REQUIRED_CONFLICT', label: '教学班必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠' },
{ key: 'TEACHER_CONFLICT', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程,或教员历不可排课时段已有课程' },
{ key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用,或场地历不可用时段已有课程' }
{ key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用,或场地历不可用时段已有课程' },
{ key: 'GUARANTEE_CONFLICT', label: '教学保障资源冲突检查', desc: '同一保障项目同一时间段请求数量超过可用能力' },
{ key: 'EVENT_CONFLICT', label: '院历/班历活动事件冲突检查', desc: '不可排课的活动事件时段存在已排课次' }
]
export default {
@@ -289,12 +289,44 @@
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="教研室代号" prop="jysdh">
<el-input v-model="editForm.jysdh" placeholder="教研室代号" clearable />
<el-select
v-model="editForm.jysdh"
filterable
clearable
placeholder="选择教研室"
style="width: 100%"
:loading="officeLoading"
>
<el-option
v-for="item in officeOptions"
:key="item.jysdh"
:label="item.jysmc ? `${item.jysmc}${item.jysdh}` : item.jysdh"
:value="item.jysdh"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划教员编号">
<el-input v-model="editForm.jysjhjybh" placeholder="教研室计划教员编号" clearable />
<el-select
v-model="editForm.jysjhjybh"
filterable
remote
clearable
reserve-keyword
placeholder="按教员姓名 / 编号搜索"
style="width: 100%"
:remote-method="searchTeacherOptions"
:loading="teacherLoading"
@focus="searchTeacherOptions('')"
>
<el-option
v-for="item in teacherOptions"
:key="item.jybh"
:label="teacherOptionLabel(item)"
:value="item.jybh"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -449,6 +481,7 @@ import {
} from '@/api/studentRecords/classCalendar'
import { listKb } from '@/api/teachOffice/kb'
import { listTeacher } from '@/api/teachOffice/teacher'
import { listOffice } from '@/api/teachOffice/office'
import { listClassroom } from '@/api/teachBusiness/classroom'
const TASK_CLIPBOARD_KEY = 'education.classTaskClipboard'
@@ -480,11 +513,13 @@ export default {
// 以下三列在库中「非空且无默认值」,留空会写入无意义值,这里前置拦截
xs: [{ required: true, message: '请输入学时', trigger: 'blur' }],
klx: [{ required: true, message: '请输入课类型', trigger: 'blur' }],
jysdh: [{ required: true, message: '请输入教研室代号', trigger: 'blur' }]
jysdh: [{ required: true, message: '请选择教研室', trigger: 'change' }]
},
kbOptions: [],
kbLoading: false,
teacherOptions: [],
officeOptions: [],
officeLoading: false,
teacherLoading: false,
roomOptions: [],
roomLoading: false,
@@ -551,6 +586,7 @@ export default {
if (val) {
this.clipboardCount = this.readClipboard().length
this.loadData()
this.loadOfficeOptions()
}
}
},
@@ -715,6 +751,8 @@ export default {
this.editForm = form
this.ensureKbOption(form.kbh, row)
this.ensureTeacherOption(form.jybh, row)
this.ensureTeacherOption(form.jysjhjybh, row)
this.ensureOfficeOption(form.jysdh)
this.ensureRoomOption(form.jsbh, row)
this.editVisible = true
this.$nextTick(() => {
@@ -767,7 +805,7 @@ export default {
ensureTeacherOption(jybh, row) {
if (!jybh) return
if (this.teacherOptions.some(item => item.jybh === jybh)) return
this.teacherOptions = [{ jybh, jyxm: (row && row.jyxm) || '' }].concat(this.teacherOptions)
this.teacherOptions = [{ jybh, jyxm: (row && (row.jyxm || row.jysjhjyxm)) || '' }].concat(this.teacherOptions)
},
searchTeacherOptions(keyword) {
this.teacherLoading = true
@@ -778,12 +816,35 @@ export default {
const data = (res && res.data) || {}
this.teacherOptions = data.records || (Array.isArray(data) ? data : [])
this.ensureTeacherOption(this.editForm && this.editForm.jybh)
this.ensureTeacherOption(this.editForm && this.editForm.jysjhjybh)
this.teacherLoading = false
}).catch(() => {
this.teacherLoading = false
})
},
/* ---------- 教研室下拉 ---------- */
loadOfficeOptions() {
if (this.officeOptions.length) return
this.officeLoading = true
listOffice({ pageNum: 1, pageSize: 500 }).then(res => {
const rows = ((res && res.data) || {}).records || []
const seen = {}
this.officeOptions = rows
.filter(r => r.jysdh && !seen[r.jysdh] && (seen[r.jysdh] = true))
.map(r => ({ jysdh: r.jysdh, jysmc: r.jysmc }))
this.ensureOfficeOption(this.editForm && this.editForm.jysdh)
this.officeLoading = false
}).catch(() => {
this.officeLoading = false
})
},
ensureOfficeOption(jysdh) {
if (!jysdh) return
if (this.officeOptions.some(item => item.jysdh === jysdh)) return
this.officeOptions = [{ jysdh, jysmc: jysdh }].concat(this.officeOptions)
},
/* ---------- 教室下拉(教学场地远程检索) ---------- */
roomOptionLabel(item) {
const name = item.jsmc || ''