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
@@ -135,6 +135,10 @@ public class KCBServiceImpl implements KCBService {
@Override @Override
public void exportSemesterGrades(TimetableQuery cond, HttpServletResponse response) { public void exportSemesterGrades(TimetableQuery cond, HttpServletResponse response) {
// 成绩表按学员粒度返回且不支持教员过滤,教员账号不能导出
if (jwglRoleHelper.isTeacher()) {
throw new BusinessException(BasicManagementConstants.FORBIDDEN, "教员账号不能导出学期成绩表");
}
TimetableQuery filter = prepareFilter(cond); TimetableQuery filter = prepareFilter(cond);
List<SemesterGradeExportVO> list = kcbTimetableMapper.selectSemesterGrades(filter); List<SemesterGradeExportVO> list = kcbTimetableMapper.selectSemesterGrades(filter);
ExcelUtil<SemesterGradeExportVO> util = new ExcelUtil<>(SemesterGradeExportVO.class); ExcelUtil<SemesterGradeExportVO> util = new ExcelUtil<>(SemesterGradeExportVO.class);
@@ -183,24 +187,27 @@ public class KCBServiceImpl implements KCBService {
result.setRq(viewDate.format(DATE_FORMAT)); result.setRq(viewDate.format(DATE_FORMAT));
result.setStartDate(start.format(DATE_FORMAT)); result.setStartDate(start.format(DATE_FORMAT));
result.setEndDate(end.format(DATE_FORMAT)); result.setEndDate(end.format(DATE_FORMAT));
// 今日/本周按日期范围取数不锁年度(翻页可跨年),角色限定与组合查询一致
TimetableQuery scope = applyRoleScope(new TimetableQuery());
result.setList(kcbTimetableMapper.selectLessonTimetable( result.setList(kcbTimetableMapper.selectLessonTimetable(
null, result.getStartDate(), result.getEndDate())); scope, result.getStartDate(), result.getEndDate()));
return result; return result;
} }
/** /**
* 未传年度时按当前学期填充;队别班次名称去空白。 * 角色数据范围:学员强制只看本队;教员强制只看本人课程;
* 学员强制只看本人所在学员队课表 * 教研室未指定时代号时按本室过滤;其余字段去空白
* <p>今日/本周课表与组合查询共用,不附带年度默认值。</p>
*/ */
private TimetableQuery prepareFilter(TimetableQuery cond) { private TimetableQuery applyRoleScope(TimetableQuery filter) {
TimetableQuery filter = cond == null ? new TimetableQuery() : cond;
if (jwglRoleHelper.isStudent()) { if (jwglRoleHelper.isStudent()) {
filter.setXydbh(jwglRoleHelper.requireStudentTeamId()); filter.setXydbh(jwglRoleHelper.requireStudentTeamId());
filter.setXydmc(null); filter.setXydmc(null);
filter.setXybh(null); filter.setXybh(null);
filter.setJybh(null); filter.setJybh(null);
filter.setJybhs(null); filter.setJybhs(null);
} else { return filter;
}
if (StringUtils.isNotEmpty(filter.getXybh())) { if (StringUtils.isNotEmpty(filter.getXybh())) {
filter.setXydbh(resolveTeamByStudent(filter.getXybh().trim())); filter.setXydbh(resolveTeamByStudent(filter.getXybh().trim()));
filter.setXydmc(null); filter.setXydmc(null);
@@ -227,16 +234,23 @@ public class KCBServiceImpl implements KCBService {
if (StringUtils.isNotEmpty(filter.getJysdh())) { if (StringUtils.isNotEmpty(filter.getJysdh())) {
filter.setJysdh(filter.getJysdh().trim()); filter.setJysdh(filter.getJysdh().trim());
} }
return filter;
} }
/**
* 未传年度时按当前学期填充;队别班次名称去空白。
* 学员强制只看本人所在学员队课表。
*/
private TimetableQuery prepareFilter(TimetableQuery cond) {
TimetableQuery filter = applyRoleScope(cond == null ? new TimetableQuery() : cond);
if (filter.getNd() != null) { if (filter.getNd() != null) {
return filter; return filter;
} }
SemesterInitializationVO semester = systemInitializationMapper.selectCurrentSemester(); SemesterInitializationVO semester = systemInitializationMapper.selectCurrentSemester();
if (semester != null && semester.getYear() != null) { if (semester != null && semester.getYear() != null) {
filter.setNd(semester.getYear()); filter.setNd(semester.getYear());
if (filter.getXqdc() == null) { // 不默认学期第次:课程-学员队关联的「学期第次」与校历学期序号口径不同,
filter.setXqdc(semester.getSequenceNumber()); // 年度已是学期代号(如 202603),再叠加序号过滤会查不到任何数据。
}
} else { } else {
filter.setNd(LocalDate.now().getYear()); filter.setNd(LocalDate.now().getYear());
} }
@@ -6,7 +6,10 @@
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :xs="24" :md="8"> <el-col :xs="24" :md="8">
<el-form-item label="教学楼代号"> <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-form-item>
</el-col> </el-col>
<el-col :xs="24" :md="8"> <el-col :xs="24" :md="8">
@@ -71,9 +74,6 @@
<el-form-item label="地点"> <el-form-item label="地点">
<el-input v-model="dialog.form.dd" placeholder="请输入地点" maxlength="50" show-word-limit clearable /> <el-input v-model="dialog.form.dd" placeholder="请输入地点" maxlength="50" show-word-limit clearable />
</el-form-item> </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-form-item label="备注">
<el-input v-model="dialog.form.bz" type="textarea" :rows="3" placeholder="请输入备注" maxlength="100" <el-input v-model="dialog.form.bz" type="textarea" :rows="3" placeholder="请输入备注" maxlength="100"
show-word-limit /> show-word-limit />
@@ -150,6 +150,7 @@ export default {
dd: '' dd: ''
}, },
tableData: [], tableData: [],
jxldhOptions: [],
selectedRows: [], selectedRows: [],
total: 0, total: 0,
pageNum: 1, pageNum: 1,
@@ -183,6 +184,7 @@ export default {
}, },
created() { created() {
this.fetchList() this.fetchList()
this.loadJxldhOptions()
}, },
methods: { 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() { handleQuery() {
this.pageNum = 1 this.pageNum = 1
this.fetchList() this.fetchList()
@@ -295,8 +310,9 @@ export default {
jxlmc: (form.jxlmc || '').trim() jxlmc: (form.jxlmc || '').trim()
} }
if (this.dialog.isEdit) payload.id = form.id 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 => { Object.keys(optional).forEach(key => {
const value = (optional[key] === null || optional[key] === undefined) ? '' : String(optional[key]).trim() const value = (optional[key] === null || optional[key] === undefined) ? '' : String(optional[key]).trim()
if (value !== '') payload[key] = value if (value !== '') payload[key] = value
@@ -317,6 +333,7 @@ export default {
this.dialog.submitting = false this.dialog.submitting = false
this.dialog.visible = false this.dialog.visible = false
this.fetchList() this.fetchList()
this.loadJxldhOptions()
}).catch(() => { }).catch(() => {
this.dialog.submitting = false this.dialog.submitting = false
}) })
@@ -356,6 +373,7 @@ export default {
this.$message.success('已删除 ' + deleteCount + ' 条教学楼数据') this.$message.success('已删除 ' + deleteCount + ' 条教学楼数据')
} }
this.fetchList() this.fetchList()
this.loadJxldhOptions()
}).catch(() => { }).catch(() => {
this.loading = false this.loading = false
this.fetchList() this.fetchList()
@@ -432,6 +450,7 @@ export default {
const skipTip = data.skipCount > 0 ? ',跳过 ' + data.skipCount + ' 条已存在代号' : '' const skipTip = data.skipCount > 0 ? ',跳过 ' + data.skipCount + ' 条已存在代号' : ''
this.$message.success('导入成功,共导入 ' + (data.insertCount || 0) + ' 条' + skipTip) this.$message.success('导入成功,共导入 ' + (data.insertCount || 0) + ' 条' + skipTip)
this.fetchList() this.fetchList()
this.loadJxldhOptions()
}).catch(() => { }).catch(() => {
this.importDialog.importing = false this.importDialog.importing = false
}) })
@@ -92,7 +92,7 @@
<div class="list-title">{{ pageTitle }}</div> <div class="list-title">{{ pageTitle }}</div>
<div class="list-actions"> <div class="list-actions">
<el-button type="primary" plain size="mini" @click="handleExportPlan">学期教学实施计划</el-button> <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> <el-button type="primary" plain size="mini" @click="handleExportCourses">课程列表另存Excel</el-button>
</div> </div>
</div> </div>
@@ -125,6 +125,7 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'
import { listToday, listWeek, listClass, exportClassPlan, exportClassGrades, exportClassCourses } from '@/api/teachBusiness/timetable' import { listToday, listWeek, listClass, exportClassPlan, exportClassGrades, exportClassCourses } from '@/api/teachBusiness/timetable'
import { listTeam } from '@/api/studentRecords/team' import { listTeam } from '@/api/studentRecords/team'
@@ -167,6 +168,13 @@ export default {
} }
}, },
computed: { 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() { selectedTodayDate() {
const date = new Date(this.todayDate) const date = new Date(this.todayDate)
date.setDate(date.getDate() + this.dayOffset) date.setDate(date.getDate() + this.dayOffset)
@@ -195,6 +203,10 @@ export default {
this.loadTeamOptions() this.loadTeamOptions()
this.getTodayList() this.getTodayList()
this.getWeekList() this.getWeekList()
// 教员不依赖队别班次选择,直接加载本人课程
if (this.isTeacher) {
this.getShiftList()
}
}, },
methods: { methods: {
weekDayName(d) { weekDayName(d) {
@@ -308,9 +320,11 @@ export default {
}, },
/* ---------- 班次:拉取数据 ---------- */ /* ---------- 班次:拉取数据 ---------- */
getShiftList() { getShiftList() {
if (!this.searchForm.teamClass) return if (!this.searchForm.teamClass && !this.isTeacher) return
const xydbh = this.teamMap[this.searchForm.teamClass] || this.searchForm.teamClass const params = { pageNum: 1, pageSize: 1000 }
const params = { xydbh: xydbh, pageNum: 1, pageSize: 1000 } if (this.searchForm.teamClass) {
params.xydbh = this.teamMap[this.searchForm.teamClass] || this.searchForm.teamClass
}
this.loading = true this.loading = true
listClass(params).then(response => { listClass(params).then(response => {
const data = response.data || {} const data = response.data || {}
@@ -323,7 +337,7 @@ export default {
}, },
/* ---------- 班次:查询 ---------- */ /* ---------- 班次:查询 ---------- */
handleQuery() { handleQuery() {
if (!this.searchForm.teamClass) { if (!this.searchForm.teamClass && !this.isTeacher) {
this.$message.warning('请先选择队别班次') this.$message.warning('请先选择队别班次')
return return
} }
@@ -340,7 +354,8 @@ export default {
window.URL.revokeObjectURL(link.href) window.URL.revokeObjectURL(link.href)
}, },
shiftQueryParams() { 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 const xydbh = this.teamMap[this.searchForm.teamClass] || this.searchForm.teamClass
return { xydbh: xydbh } return { xydbh: xydbh }
}, },
@@ -117,14 +117,18 @@
import { checkConflict, checkAllConflict, resetConflict, getConflictDetails } from '@/api/teachBusiness/timetableConflict' import { checkConflict, checkAllConflict, resetConflict, getConflictDetails } from '@/api/teachBusiness/timetableConflict'
import { listAllSemester } from '@/api/teachBusiness/semester' import { listAllSemester } from '@/api/teachBusiness/semester'
// 类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量; // 类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量;
// 检查状态与冲突数完全来自后端接口) // 检查状态与冲突数完全来自后端接口)
// 注意:教员卡片含「教员历不可排」、教室卡片含「场地历不可用」,与排课窗口硬冲突口径一致 // 注意:教员卡片含「教员历不可排」、教室卡片含「场地历不可用」,与排课窗口硬冲突口径一致
// 保障资源、院历/班历事件两个维度也必须列出——后端 checkAll 会全部执行,缺卡片会导致
// 冲突总数计入消息却找不到对应卡片回显明细
const CARD_DEFS = [ const CARD_DEFS = [
{ key: 'TEAM_CONFLICT', label: '教学班时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程' }, { key: 'TEAM_CONFLICT', label: '教学班时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程' },
{ key: 'ELECTIVE_REQUIRED_CONFLICT', label: '教学班必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠' }, { key: 'ELECTIVE_REQUIRED_CONFLICT', label: '教学班必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠' },
{ key: 'TEACHER_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 { export default {
@@ -289,12 +289,44 @@
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="教研室代号" prop="jysdh"> <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-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="计划教员编号"> <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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -449,6 +481,7 @@ import {
} from '@/api/studentRecords/classCalendar' } from '@/api/studentRecords/classCalendar'
import { listKb } from '@/api/teachOffice/kb' import { listKb } from '@/api/teachOffice/kb'
import { listTeacher } from '@/api/teachOffice/teacher' import { listTeacher } from '@/api/teachOffice/teacher'
import { listOffice } from '@/api/teachOffice/office'
import { listClassroom } from '@/api/teachBusiness/classroom' import { listClassroom } from '@/api/teachBusiness/classroom'
const TASK_CLIPBOARD_KEY = 'education.classTaskClipboard' const TASK_CLIPBOARD_KEY = 'education.classTaskClipboard'
@@ -480,11 +513,13 @@ export default {
// 以下三列在库中「非空且无默认值」,留空会写入无意义值,这里前置拦截 // 以下三列在库中「非空且无默认值」,留空会写入无意义值,这里前置拦截
xs: [{ required: true, message: '请输入学时', trigger: 'blur' }], xs: [{ required: true, message: '请输入学时', trigger: 'blur' }],
klx: [{ required: true, message: '请输入课类型', trigger: 'blur' }], klx: [{ required: true, message: '请输入课类型', trigger: 'blur' }],
jysdh: [{ required: true, message: '请输入教研室代号', trigger: 'blur' }] jysdh: [{ required: true, message: '请选择教研室', trigger: 'change' }]
}, },
kbOptions: [], kbOptions: [],
kbLoading: false, kbLoading: false,
teacherOptions: [], teacherOptions: [],
officeOptions: [],
officeLoading: false,
teacherLoading: false, teacherLoading: false,
roomOptions: [], roomOptions: [],
roomLoading: false, roomLoading: false,
@@ -551,6 +586,7 @@ export default {
if (val) { if (val) {
this.clipboardCount = this.readClipboard().length this.clipboardCount = this.readClipboard().length
this.loadData() this.loadData()
this.loadOfficeOptions()
} }
} }
}, },
@@ -715,6 +751,8 @@ export default {
this.editForm = form this.editForm = form
this.ensureKbOption(form.kbh, row) this.ensureKbOption(form.kbh, row)
this.ensureTeacherOption(form.jybh, row) this.ensureTeacherOption(form.jybh, row)
this.ensureTeacherOption(form.jysjhjybh, row)
this.ensureOfficeOption(form.jysdh)
this.ensureRoomOption(form.jsbh, row) this.ensureRoomOption(form.jsbh, row)
this.editVisible = true this.editVisible = true
this.$nextTick(() => { this.$nextTick(() => {
@@ -767,7 +805,7 @@ export default {
ensureTeacherOption(jybh, row) { ensureTeacherOption(jybh, row) {
if (!jybh) return if (!jybh) return
if (this.teacherOptions.some(item => item.jybh === 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) { searchTeacherOptions(keyword) {
this.teacherLoading = true this.teacherLoading = true
@@ -778,12 +816,35 @@ export default {
const data = (res && res.data) || {} const data = (res && res.data) || {}
this.teacherOptions = data.records || (Array.isArray(data) ? data : []) this.teacherOptions = data.records || (Array.isArray(data) ? data : [])
this.ensureTeacherOption(this.editForm && this.editForm.jybh) this.ensureTeacherOption(this.editForm && this.editForm.jybh)
this.ensureTeacherOption(this.editForm && this.editForm.jysjhjybh)
this.teacherLoading = false this.teacherLoading = false
}).catch(() => { }).catch(() => {
this.teacherLoading = false 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) { roomOptionLabel(item) {
const name = item.jsmc || '' const name = item.jsmc || ''