1、校历的78、晚上、夜间等直接落库,

2、配当、任务书完善、排课窗完善;
3、任务书批量发布、删除等;
4、班历同步校历
This commit is contained in:
2026-09-18 10:38:39 +08:00
parent 911dfe911b
commit f3ae9fb2fd
62 changed files with 3508 additions and 143 deletions
@@ -57,3 +57,14 @@ export function allocationExport(xydxqbhList) {
responseType: 'blob'
})
}
// 自动排布:把铺学时建议的计算起始周回填到 pdqsz;
// onlyMissing=true(辅助排布)只补未设起始周的任务;bhList 限定所选任务
export function allocationAutoArrange(xydxqbh, onlyMissing, bhList) {
return request({
url: '/teachingAllocation/autoArrange',
method: 'post',
params: { xydxqbh, onlyMissing: !!onlyMissing },
data: bhList && bhList.length ? bhList : null
})
}
+98
View File
@@ -0,0 +1,98 @@
import request from '@/utils/request'
// 「我的教学」角色视图接口(阶段 CMyBusinessController /my
// 教员/教研室/学员身份全部由服务端按登录账号解析,不传他人编号
// ==================== 教员 ====================
/** 本教员课程任务 GET /my/teacher/courses?nd=&xqdc= */
export function myTeacherCourses(params) {
return request({
url: '/my/teacher/courses',
method: 'get',
params
})
}
/** 本教员实施计划 GET /my/teacher/lessons?start=&end=&nd= */
export function myTeacherLessons(params) {
return request({
url: '/my/teacher/lessons',
method: 'get',
params
})
}
/** 本教员课表网格 GET /my/teacher/grid?mbbh=&start=&end=&nd= */
export function myTeacherGrid(params) {
return request({
url: '/my/teacher/grid',
method: 'get',
params
})
}
// ==================== 教研室 ====================
/** 今日本室计划 GET /my/office/today?rq=&offset= */
export function myOfficeToday(params) {
return request({
url: '/my/office/today',
method: 'get',
params
})
}
/** 本室课程 GET /my/office/courses?nd=&xqdc= */
export function myOfficeCourses(params) {
return request({
url: '/my/office/courses',
method: 'get',
params
})
}
/** 本室教员清单 GET /my/office/teachers */
export function myOfficeTeachers() {
return request({
url: '/my/office/teachers',
method: 'get'
})
}
// ==================== 学员 ====================
/** 本学员队课程列表 GET /my/student/courses?nd=&xqdc= */
export function myStudentCourses(params) {
return request({
url: '/my/student/courses',
method: 'get',
params
})
}
/** 本学员队课次 GET /my/student/lessons?start=&end= */
export function myStudentLessons(params) {
return request({
url: '/my/student/lessons',
method: 'get',
params
})
}
/** 本学员队课表网格 GET /my/student/grid?mbbh=&start=&end=&nd= */
export function myStudentGrid(params) {
return request({
url: '/my/student/grid',
method: 'get',
params
})
}
/** 本学员队干部清单 GET /my/team/cadres */
export function myTeamCadres() {
return request({
url: '/my/team/cadres',
method: 'get'
})
}
@@ -23,6 +23,25 @@ export function arrangeLessons(data) {
})
}
// 拖拽移动课次:{ sskcbh, from: {rq,jc}, to: {rq,jc} };返回 {moved, warning?}
export function moveLesson(data) {
return request({
url: '/schedulingWindow/move',
method: 'post',
data
})
}
// 导出排课窗视图 Excel(每周一个 sheet
export function exportSchedulingView(xydxqbh) {
return request({
url: '/schedulingWindow/export',
method: 'get',
params: { xydxqbh },
responseType: 'blob'
})
}
// 删除所选节次(仅删除该课程在这些格上的课次;已提交实施计划的课次会被拒绝)
export function deleteLessonCells(data) {
return request({
@@ -0,0 +1,26 @@
import request from '@/utils/request'
// 读取教务系统参数(DBVersion 单行;显示开关 xs78J/xsws/xsyj 等)
export function getSystemSettings() {
return request({
url: '/systemSettings/get',
method: 'get'
})
}
// 更新系统参数(后端只写非空字段,可局部提交)
export function updateSystemSettings(data) {
return request({
url: '/systemSettings/update',
method: 'post',
data
})
}
// 节次段字典:节次时间表的双节次行(jcsy 节次索引、sd 时段),空表时前端回退默认六段
export function getPeriodSlots() {
return request({
url: '/systemSettings/periodSlots',
method: 'get'
})
}
+49 -4
View File
@@ -5,12 +5,12 @@ import request from '@/utils/request'
* 接口依据:/taskBookFill/*(前端 baseURL 为 /api,此处不重复 /api 前缀)
*/
/** 填报列表 GET /taskBookFill/list?jxrwbh= */
export function taskBookList(jxrwbh) {
/** 填报列表 GET /taskBookFill/list?jxrwbh=&jybh= */
export function taskBookList(jxrwbh, jybh) {
return request({
url: '/taskBookFill/list',
method: 'get',
params: { jxrwbh }
params: { jxrwbh, jybh }
})
}
@@ -59,7 +59,7 @@ export function taskBookSetRoom(data) {
})
}
/** 填报字段 POST /taskBookFill/fill body: { bh, jysjhjybh, jsbh, jysjhbz } */
/** 填报字段 POST /taskBookFill/fill body: { bh, jysjhjybh, jsbh, jysjhbz, kcxh } */
export function taskBookFill(data) {
return request({
url: '/taskBookFill/fill',
@@ -67,3 +67,48 @@ export function taskBookFill(data) {
data
})
}
/** 批量指定责任教员 POST /taskBookFill/batchSetTeacher body: { bhList, mode, jybh } */
export function taskBookBatchSetTeacher(data) {
return request({
url: '/taskBookFill/batchSetTeacher',
method: 'post',
data
})
}
/** 批量指定场地 POST /taskBookFill/batchSetRoom body: { bhList, jsbh, useSpecial } */
export function taskBookBatchSetRoom(data) {
return request({
url: '/taskBookFill/batchSetRoom',
method: 'post',
data
})
}
/** 批量填报 POST /taskBookFill/batchFill body: { bhList, jysjhjybh?, jsbh?, useSpecial?, jysjhbz?, kcxh? } */
export function taskBookBatchFill(data) {
return request({
url: '/taskBookFill/batchFill',
method: 'post',
data
})
}
/** 行级删除 POST /taskBookFill/deleteRows body: { bhList } */
export function taskBookDeleteRows(bhList) {
return request({
url: '/taskBookFill/deleteRows',
method: 'post',
data: { bhList }
})
}
/** 教研室跨任务汇总 GET /taskBookFill/officeSummary?jysdh= */
export function taskBookOfficeSummary(jysdh) {
return request({
url: '/taskBookFill/officeSummary',
method: 'get',
params: { jysdh }
})
}
+18 -6
View File
@@ -76,16 +76,28 @@ export function deleteTeachingTask(bh) {
}
/**
* 教学任务发布(需要先填写教研室任务书)
* POST /teachingTask/batchPublish?bh=
* @param bh 教学任务编号
* 返回:更新数量
* 批量删除教学任务(级联删除教研室任务书)
* POST /teachingTask/batchDelete body: [bh, ...]
*/
export function publishTeachingTask(bh) {
export function batchDeleteTeachingTask(bhList) {
return request({
url: '/teachingTask/batchDelete',
method: 'post',
data: bhList
})
}
/**
* 教学任务批量发布(需要先填写教研室任务书)
* POST /teachingTask/batchPublish body: [bh, ...]
* @param bhList 教学任务编号数组(单个任务传 [bh])
* 返回:成功发布的任务数
*/
export function publishTeachingTask(bhList) {
return request({
url: '/teachingTask/batchPublish',
method: 'post',
params: { bh }
data: bhList
})
}
@@ -114,3 +114,29 @@ export function exportTimetableGrid(params) {
responseType: 'blob'
})
}
/**
* 有课/无课时段查询
* GET /kcb/freeSlots?dim=&id=&start=&end=
* 返回 FreePeriodsVOperiods / days[].busy / days[].free / days[].blocked
*/
export function getFreeSlots(params) {
return request({
url: '/kcb/freeSlots',
method: 'get',
params
})
}
/**
* 课次综合查询
* GET /kcb/lessons?nd=&xqdc=&xydbh=&xydmc=&jybh=&jsbh=&jysdh=&xybh=&start=&end=
* 返回 List<DailyWeeklyTimetableVO>
*/
export function listLessons(params) {
return request({
url: '/kcb/lessons',
method: 'get',
params
})
}
@@ -25,12 +25,13 @@ export function batchSaveClassCalendarEvent(xydxqbh, cells) {
})
}
// 重新同步校历:confirm=false 返回缺失格预览,confirm=true 实际补齐
export function resyncClassCalendar(xydxqbh, confirm) {
// 重新同步校历:confirm=false 返回差异预览,confirm=true 实际写入;
// force=true 时额外覆盖与校历不一致的已有格
export function resyncClassCalendar(xydxqbh, confirm, force) {
return request({
url: '/class-calendar/resync',
method: 'post',
params: { xydxqbh, confirm }
params: { xydxqbh, confirm, force: !!force }
})
}
@@ -0,0 +1,154 @@
<template>
<div class="app-container">
<el-tabs v-model="tab" type="border-card">
<!-- ==================== 我的课表 ==================== -->
<el-tab-pane label="我的课表" name="grid">
<el-form inline size="small" style="margin-bottom: 8px">
<el-form-item label="日期范围">
<el-date-picker
v-model="gridRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="~"
start-placeholder="开始"
end-placeholder="结束"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadGrid">生成</el-button>
</el-form-item>
</el-form>
<timetable-grid-view :grid="grid" :loading="gridLoading" :show-teacher="true" :show-room="true" />
</el-tab-pane>
<!-- ==================== 我的课程 ==================== -->
<el-tab-pane label="我的课程" name="courses">
<el-form inline size="small" style="margin-bottom: 8px">
<el-form-item label="年度">
<el-input-number v-model="courseQuery.nd" :min="2000" :max="2100" style="width: 120px" />
</el-form-item>
<el-form-item label="学期第次">
<el-input-number v-model="courseQuery.xqdc" :min="1" :max="4" style="width: 100px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadCourses">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="courseLoading" :data="courseRows" border stripe size="small" max-height="560">
<el-table-column prop="kcmcksxs" label="课程名称/课时系数" min-width="180" align="center" show-overflow-tooltip />
<el-table-column prop="zrjykc" label="责任教员/课次" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="jhxs" label="计划学时" width="80" align="center" />
<el-table-column prop="yxxs" label="运行学时" width="80" align="center" />
<el-table-column prop="khlxfs" label="考核类型/方式" width="120" align="center" />
<el-table-column prop="ssjy" label="实施教员" width="110" align="center" />
<el-table-column prop="rscd" label="人数/场地" min-width="140" align="center" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<!-- ==================== 日实施计划 ==================== -->
<el-tab-pane label="日实施计划" name="lessons">
<el-form inline size="small" style="margin-bottom: 8px">
<el-form-item label="日期范围">
<el-date-picker
v-model="lessonRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="~"
start-placeholder="开始"
end-placeholder="结束"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadLessons">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="lessonLoading" :data="lessonRows" border stripe size="small" max-height="560">
<el-table-column prop="sksj" label="上课时间" width="170" align="center" />
<el-table-column prop="kc" label="课程" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="jy" label="教员" width="120" align="center" />
<el-table-column prop="cd" label="场地" width="120" align="center" />
<el-table-column prop="jxnrxff" label="教学内容 / 方法" min-width="160" align="center" show-overflow-tooltip />
<el-table-column prop="bz" label="备注" min-width="100" align="center" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<!-- ==================== 本队干部 ==================== -->
<el-tab-pane label="本队干部" name="cadres">
<el-table v-loading="cadreLoading" :data="cadreRows" border stripe size="small" max-height="560">
<el-table-column prop="xh" label="学号" width="140" align="center" />
<el-table-column prop="xm" label="姓名" width="140" align="center" />
<el-table-column prop="ggrz" label="骨干任职" min-width="160" align="center" />
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import { myStudentCourses, myStudentLessons, myStudentGrid, myTeamCadres } from '@/api/teachBusiness/my'
import TimetableGridView from '@/components/TimetableGridView'
export default {
name: 'MyClass',
components: { TimetableGridView },
data() {
return {
tab: 'grid',
gridRange: null,
grid: null,
gridLoading: false,
courseQuery: { nd: null, xqdc: null },
courseRows: [],
courseLoading: false,
lessonRange: null,
lessonRows: [],
lessonLoading: false,
cadreRows: [],
cadreLoading: false
}
},
created() {
this.loadGrid()
this.loadCourses()
this.loadCadres()
},
methods: {
loadGrid() {
this.gridLoading = true
const params = {}
if (this.gridRange && this.gridRange.length === 2) {
params.start = this.gridRange[0]
params.end = this.gridRange[1]
}
myStudentGrid(params)
.then(res => { this.grid = res.data })
.finally(() => { this.gridLoading = false })
},
loadCourses() {
this.courseLoading = true
myStudentCourses(this.courseQuery)
.then(res => { this.courseRows = res.data || [] })
.finally(() => { this.courseLoading = false })
},
loadLessons() {
this.lessonLoading = true
const params = {}
if (this.lessonRange && this.lessonRange.length === 2) {
params.start = this.lessonRange[0]
params.end = this.lessonRange[1]
}
myStudentLessons(params)
.then(res => { this.lessonRows = res.data || [] })
.finally(() => { this.lessonLoading = false })
},
loadCadres() {
this.cadreLoading = true
myTeamCadres()
.then(res => { this.cadreRows = res.data || [] })
.finally(() => { this.cadreLoading = false })
}
}
}
</script>
@@ -20,6 +20,7 @@
<el-input v-model="toolbar.eventName" size="small" class="sce-event-input" :placeholder="mode === 'class' ? '请输入班历事件名称' : (mode === 'venue' ? '请输入场地事件名称' : '请输入校历事件名称')" />
<el-checkbox v-if="mode !== 'venue'" v-model="toolbar.bold" class="sce-cb">加粗显示</el-checkbox>
<el-checkbox v-model="toolbar.schedulable" class="sce-cb">可排课</el-checkbox>
<el-checkbox v-if="mode !== 'venue'" v-model="toolbar.autoSchedule" class="sce-cb">自动排课</el-checkbox>
<el-checkbox v-if="mode !== 'venue'" v-model="toolbar.mainCourse" class="sce-cb">正课</el-checkbox>
<el-checkbox v-if="mode !== 'venue'" v-model="toolbar.remarkShow" class="sce-cb">备注显示</el-checkbox>
<span class="sce-label">备注:</span>
@@ -148,25 +149,47 @@ import {
listVenueLessons,
listVenueDesignatedTeams
} from '@/api/teachBusiness/venueCalendar'
import { getSystemSettings, updateSystemSettings, getPeriodSlots } from '@/api/teachBusiness/systemSettings'
// 节次定义12/34/56 常显,78/晚上/夜间由开关控制可见
const SLOT_DEFS = [
{ key: '12', label: '1-2', ctrl: null },
{ key: '34', label: '3-4', ctrl: null },
{ key: '56', label: '5-6', ctrl: null },
{ key: '78', label: '7-8', ctrl: 'show78' },
{ key: 'night', label: '晚上', ctrl: 'showNight' },
{ key: 'late', label: '夜间', ctrl: 'showLateNight' }
// 节次定义回退值:节次时间表为空时使用,12/34/56 常显,78/晚上/夜间由开关控制可见
const DEFAULT_SLOT_DEFS = [
{ key: '12', label: '1-2', courseClass: '1-2', ctrl: null },
{ key: '34', label: '3-4', courseClass: '3-4', ctrl: null },
{ key: '56', label: '5-6', courseClass: '5-6', ctrl: null },
{ key: '78', label: '7-8', courseClass: '7-8', ctrl: 'show78' },
{ key: 'night', label: '晚上', courseClass: '9-10', ctrl: 'showNight' },
{ key: 'late', label: '夜间', courseClass: '11-12', ctrl: 'showLateNight' }
]
// 前端节次 key -> 后端 xqxlb.courseClass 节次范围(如 12 节 -> "1-2"、夜间 -> "11-12"
const SLOT_COURSE_MAP = {
'12': '1-2',
'34': '3-4',
'56': '5-6',
'78': '7-8',
night: '9-10',
late: '11-12'
const SLOT_COURSE_MAP = {}
DEFAULT_SLOT_DEFS.forEach(s => { SLOT_COURSE_MAP[s.key] = s.courseClass })
// 节次范围解析:"1-2"/"7~8" -> [1,2];单数字 -> [n,n];非数字返回 null
function parseSlotRange(label) {
if (label == null) return null
const s = String(label).trim()
const m = s.match(/(\d+)\s*[-~—–]\s*(\d+)/)
if (m) return [+m[1], +m[2]]
if (/^\d+$/.test(s)) return [+s, +s]
return null
}
// 节次段受哪个显示开关控制:前 6 节常显,7-8 -> show789-10 -> showNight11+ -> showLateNight
// 非数字标签按名称/时段推断(晚->晚上,夜->夜间)
function ctrlOfSlot(label, sd) {
const range = parseSlotRange(label)
if (range) {
const first = range[0]
if (first <= 6) return null
if (first <= 8) return 'show78'
if (first <= 10) return 'showNight'
return 'showLateNight'
}
const s = String(label || '') + String(sd || '')
if (s.indexOf('夜') >= 0) return 'showLateNight'
if (s.indexOf('晚') >= 0) return 'showNight'
return null
}
const WEEK_DAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
@@ -215,15 +238,20 @@ export default {
totalWeeks: 0,
dateRangeText: '',
weeks: [],
// 显示开关(默认不勾选,每天仅显示 1-2/3-4/5-6 三个节次
// 显示开关(默认不勾选,挂载时从系统参数 DBVersion 恢复
show78: false,
showNight: false,
showLateNight: false,
// 系统参数是否已加载(加载完成前的开关赋值不回写)
settingsReady: false,
// 节次列定义:由节次时间表驱动,空表时回退默认六段
slotDefs: DEFAULT_SLOT_DEFS.slice(),
// 工具栏表单
toolbar: {
eventName: '',
bold: false,
schedulable: false,
autoSchedule: false,
mainCourse: false,
remarkShow: false,
remark: ''
@@ -250,7 +278,6 @@ export default {
// 指定本场地为专用教室的班次(顶部横幅)
designatedTeams: [],
weekDayOptions: WEEK_DAYS,
slotOptions: SLOT_DEFS,
ruleDialog: {
visible: false,
submitting: false,
@@ -262,12 +289,16 @@ export default {
}
},
computed: {
// 规律生成弹窗的节次选项(同节次时间表口径)
slotOptions() {
return this.slotDefs
},
// 当前可见列(由开关过滤,默认每天三个节次)
visibleColumns() {
const cols = []
let colIndex = 0
WEEK_DAYS.forEach((dayLabel, dayIndex) => {
SLOT_DEFS.forEach(slot => {
this.slotDefs.forEach(slot => {
if (slot.ctrl && !this[slot.ctrl]) return
cols.push({ colIndex: colIndex, dayIndex: dayIndex, slotKey: slot.key, slotLabel: slot.label, dayLabel: dayLabel })
colIndex++
@@ -312,6 +343,10 @@ export default {
}
},
watch: {
// 显示开关变更即写回系统参数(防抖合并连续变更)
show78() { this.persistDisplaySettings() },
showNight() { this.persistDisplaySettings() },
showLateNight() { this.persistDisplaySettings() },
nd: {
immediate: true,
handler(val) {
@@ -371,10 +406,72 @@ export default {
this.selectedKeys = []
}
},
created() {
this.loadDisplaySettings()
this.loadPeriodSlots()
},
beforeDestroy() {
this.removeGlobalListeners()
if (this._settingsTimer) clearTimeout(this._settingsTimer)
},
methods: {
/* ---------- 系统参数 / 节次字典 ---------- */
// 显示开关从 DBVersion 恢复,刷新不丢配置
loadDisplaySettings() {
getSystemSettings().then(res => {
const d = (res && res.data) || {}
const pick = (a, b) => (d[a] !== undefined ? d[a] : d[b])
this.show78 = !!pick('xs78J', 'xs78j')
this.showNight = !!pick('xsws', 'xsws')
this.showLateNight = !!pick('xsyj', 'xsyj')
this.$nextTick(() => { this.settingsReady = true })
}).catch(() => {
this.settingsReady = true
})
},
// 开关变更回写 DBVersion(只提交三个字段,后端只写非空字段不影响其它参数)
persistDisplaySettings() {
if (!this.settingsReady) return
if (this._settingsTimer) clearTimeout(this._settingsTimer)
this._settingsTimer = setTimeout(() => {
updateSystemSettings({
xs78J: this.show78,
xsws: this.showNight,
xsyj: this.showLateNight
}).catch(() => {})
}, 400)
},
// 节次列由节次时间表驱动;空表或拉取失败保留默认六段
loadPeriodSlots() {
getPeriodSlots().then(res => {
const rows = (res && res.data) || []
const defs = rows
.filter(r => r && r.jcsy)
.map(r => ({
key: String(r.jcsy),
label: String(r.jcsy),
courseClass: String(r.jcsy),
ctrl: ctrlOfSlot(r.jcsy, r.sd)
}))
if (defs.length) this.slotDefs = defs
}).catch(() => {})
},
// 节次格 key -> 后端 courseClass 值(动态节次表优先)
courseClassOfSlot(slotKey) {
const s = this.slotDefs.find(x => x.key === slotKey)
return s ? s.courseClass : (SLOT_COURSE_MAP[slotKey] || null)
},
// 单节次 -> 节次格 key(动态范围优先,回退固定 1-12 映射)
jcToSlotKey(jc) {
const n = Number(jc)
if (!n) return null
for (const s of this.slotDefs) {
const r = parseSlotRange(s.courseClass)
if (r && n >= r[0] && n <= r[1]) return s.key
}
const fallback = JC_SLOT_MAP[n]
return this.slotDefs.some(x => x.key === fallback) ? fallback : null
},
/* ---------- 数据加载 ---------- */
loadClassCalendar() {
const start = (this.rangeStart || '').slice(0, 10)
@@ -504,7 +601,7 @@ export default {
const lessons = {}
lsList.forEach(item => {
const dateStr = String(item.rq || '').slice(0, 10)
const slotKey = JC_SLOT_MAP[item.jc]
const slotKey = this.jcToSlotKey(item.jc)
if (!dateStr || !slotKey) return
const key = dateStr + '#' + slotKey
if (!lessons[key]) lessons[key] = []
@@ -519,7 +616,7 @@ export default {
const merged = {}
list.forEach(row => {
const dateStr = String(row.rq || '').slice(0, 10)
const slotKey = JC_SLOT_MAP[row.jc]
const slotKey = this.jcToSlotKey(row.jc)
if (!dateStr || !slotKey) return
const pos = this.locateDateSlot(dateStr, slotKey)
if (!pos) return
@@ -610,9 +707,11 @@ export default {
return { ok, skipped }
},
periodsOfSlot(slotKey) {
const seg = SLOT_COURSE_MAP[slotKey]
if (!seg) return []
return seg.split('-').map(Number).filter(n => !isNaN(n))
const range = parseSlotRange(this.courseClassOfSlot(slotKey))
if (!range) return []
const list = []
for (let n = range[0]; n <= range[1]; n++) list.push(n)
return list
},
/* ---------- 规律生成 ---------- */
openRuleDialog() {
@@ -676,17 +775,18 @@ export default {
if (slotKey === null) return null
return { wIdx, dayIndex, slotKey }
},
// 后端 courseClass 节次范围 -> 前端节次 key
// 后端 courseClass 节次范围 -> 前端节次 key(动态节次表 + 紧凑写法兼容)
courseClassToSlotKey(courseClass) {
if (!courseClass) return null
const s = String(courseClass).trim()
for (const key in SLOT_COURSE_MAP) {
if (s === SLOT_COURSE_MAP[key]) return key
const direct = this.slotDefs.find(x => x.courseClass === s)
if (direct) return direct.key
// 兼容 "910"/"1112"/"12" 等紧凑写法:按单节次归并到所属段
if (/^\d+$/.test(s) && s.length > 1) {
const first = this.jcToSlotKey(Number(s.slice(0, s.length - 1)))
if (first) return first
}
// 兼容 "910"/"1112" 等紧凑写法
if (s === '910') return 'night'
if (s === '1112') return 'late'
return null
return this.jcToSlotKey(s)
},
// 获取日期所在周的周一(周一为一周起点)
getMonday(d) {
@@ -914,7 +1014,7 @@ export default {
name: name,
bold: this.toolbar.bold,
schedulable: this.toolbar.schedulable,
autoSchedule: !!(prev && prev.autoSchedule),
autoSchedule: !!this.toolbar.autoSchedule,
mainCourse: this.toolbar.mainCourse,
remarkShow: this.toolbar.remarkShow,
remark: this.toolbar.remark
@@ -975,6 +1075,7 @@ export default {
this.toolbar.eventName = target.name
this.toolbar.bold = !!target.bold
this.toolbar.schedulable = !!target.schedulable
this.toolbar.autoSchedule = !!target.autoSchedule
this.toolbar.mainCourse = !!target.mainCourse
this.toolbar.remarkShow = !!target.remarkShow
this.toolbar.remark = target.remark || ''
@@ -997,6 +1098,7 @@ export default {
this.toolbar.eventName = ev.name
this.toolbar.bold = !!ev.bold
this.toolbar.schedulable = !!ev.schedulable
this.toolbar.autoSchedule = !!ev.autoSchedule
this.toolbar.mainCourse = !!ev.mainCourse
this.toolbar.remarkShow = !!ev.remarkShow
this.toolbar.remark = ev.remark || ''
@@ -1132,7 +1234,7 @@ export default {
bzxs: !!ev.remarkShow,
zdpk: !!ev.autoSchedule,
zk: !!ev.mainCourse,
courseClass: SLOT_COURSE_MAP[pos.slotKey] || null,
courseClass: this.courseClassOfSlot(pos.slotKey),
xydxqbh: this.mode === 'class' ? this.xydxqbh : undefined,
xydbh: this.mode === 'class' ? this.xydbh : undefined
}
@@ -106,8 +106,9 @@
<el-radio-group v-model="viewMode" size="mini" style="margin-right: 12px">
<el-radio-button label="week">单周视图</el-radio-button>
<el-radio-button label="all">全学期视图</el-radio-button>
<el-radio-button label="h">横版视图</el-radio-button>
</el-radio-group>
<template v-if="viewMode === 'week'">
<template v-if="viewMode !== 'all'">
<el-button size="mini" icon="el-icon-arrow-left" :disabled="weekIndex <= 0" @click="weekIndex--">上一周</el-button>
<span class="week-text">
<b>{{ currentWeek ? currentWeek.weekNo : '—' }}</b>
@@ -138,6 +139,9 @@
<el-checkbox-button label="night">夜间</el-checkbox-button>
</el-checkbox-group>
<el-button size="mini" type="text" @click="conflictDims = []; periodToggles = ['p78','eve','night']">重置</el-button>
<el-checkbox v-model="moveMode" size="mini" style="margin-left: 10px">拖拽调课</el-checkbox>
<el-button size="mini" icon="el-icon-download" style="margin-left: 8px" @click="handleExport">导出</el-button>
<el-button size="mini" icon="el-icon-printer" @click="handlePrint">打印</el-button>
</div>
</div>
@@ -178,13 +182,65 @@
@mousedown.prevent="startDrag(d.fi, jc, d, $event)"
@mouseenter="hoverCell(d.fi, jc)"
@dblclick="dblclickCell(d, jc)"
@dragover="onCellDragover(d, jc, $event)"
@dragleave="onCellDragleave(d, jc)"
@drop="onCellDrop(d, jc, $event)"
>
<template v-if="lessonsOf(d, jc).length">
<div
v-for="l in lessonsOf(d, jc)"
:key="l.bh"
class="lesson"
:class="{ 'is-current': currentCourse && l.sskcbh === currentCourse.sskcbh }"
:class="{ 'is-current': currentCourse && l.sskcbh === currentCourse.sskcbh, 'is-draggable': moveMode && lessonEditable(l) }"
:draggable="moveMode && lessonEditable(l)"
@dragstart="onLessonDragstart(l, d, jc, $event)"
>
<div class="lesson-name">{{ shortName(l) }}</div>
<div class="lesson-meta">{{ [l.kcmc ? l.jxnr : '', l.jyxm, l.jsmc].filter(Boolean).join(' · ') || '—' }}</div>
</div>
</template>
<div v-else-if="isSelected(d.date, jc)" class="slot-picked">已选</div>
<div v-else-if="cellOf(d, jc) && cellOf(d, jc).unavailable" class="slot-block">{{ cellOf(d, jc).reason || '不可排' }}</div>
</div>
</div>
</template>
<!-- ==================== 横版=日期 =节次E4 ==================== -->
<template v-if="viewMode === 'h' && currentWeek">
<div class="tb-row tb-head">
<div class="tb-cell tb-corner">日期</div>
<div
v-for="jc in jcList"
:key="'hjc' + jc"
class="tb-cell tb-day"
:title="periodLabel(jc)"
>{{ jc }}</div>
</div>
<div v-for="d in hDays" :key="'hd' + d.date" class="tb-row">
<div class="tb-cell tb-jc" :class="{ 'day-off': !!d.unavailableReason }" :title="d.unavailableReason || ''">
{{ weekdayName(d.weekday) }} {{ fmtDate(d.date) }}
</div>
<div
v-for="jc in jcList"
:key="d.date + '#h#' + jc"
class="tb-cell tb-slot"
:class="slotClass(d, jc)"
:title="slotTitle(d, jc)"
@mousedown.prevent="startDrag(d.fi, jc, d, $event)"
@mouseenter="hoverCell(d.fi, jc)"
@dblclick="dblclickCell(d, jc)"
@dragover="onCellDragover(d, jc, $event)"
@dragleave="onCellDragleave(d, jc)"
@drop="onCellDrop(d, jc, $event)"
>
<template v-if="lessonsOf(d, jc).length">
<div
v-for="l in lessonsOf(d, jc)"
:key="l.bh"
class="lesson"
:class="{ 'is-current': currentCourse && l.sskcbh === currentCourse.sskcbh, 'is-draggable': moveMode && lessonEditable(l) }"
:draggable="moveMode && lessonEditable(l)"
@dragstart="onLessonDragstart(l, d, jc, $event)"
>
<div class="lesson-name">{{ shortName(l) }}</div>
<div class="lesson-meta">{{ [l.kcmc ? l.jxnr : '', l.jyxm, l.jsmc].filter(Boolean).join(' · ') || '—' }}</div>
@@ -520,6 +576,8 @@
import {
getSchedulingView,
arrangeLessons,
moveLesson,
exportSchedulingView,
deleteLessonCells,
clearCourseLessons,
deleteRunningCourse,
@@ -529,6 +587,7 @@ import {
} from '@/api/teachBusiness/schedulingWindow'
import { listSemester } from '@/api/studentRecords/semester'
import { listAllClassroom } from '@/api/teachBusiness/classroom'
import { saveAs } from 'file-saver'
export default {
name: 'SchedulingWindow',
@@ -551,8 +610,12 @@ export default {
additiveDrag: false,
/** 当前聚焦格(⑨ 当前格子编排信息) */
focusedCell: null,
/** 视图模式:week 单周 / all 全学期 */
/** 视图模式:week 单周 / all 全学期 / h 横版 */
viewMode: 'week',
/** 拖拽调课模式(E4):开启后已排课次可拖到其它格 */
moveMode: false,
/** 当前拖放悬停目标格 key */
dropTarget: null,
/** 冲突显示选项(按维度着色) */
conflictDims: [],
conflictDimOptions: [
@@ -624,8 +687,9 @@ export default {
})
return { list, index }
},
/** 网格行:单周一行 / 全学期每周一组 */
/** 网格行:单周一行 / 全学期每周一组;横版走 hDays */
gridRows() {
if (this.viewMode === 'h') return []
if (this.viewMode === 'all') {
return this.weeks.map(w => ({
key: 'w' + w.weekNo,
@@ -641,6 +705,12 @@ export default {
days: (w.days || []).map(d => Object.assign({}, d, { fi: this.fiOf(d.date) }))
}]
},
/** 横版视图的当前周日列表(行=日期) */
hDays() {
const w = this.currentWeek
if (!w) return []
return (w.days || []).map(d => Object.assign({}, d, { fi: this.fiOf(d.date) }))
},
/** 节次字典(来自节次时间表),缺失时按数据推导 */
periodDict() {
const list = (this.view && this.view.periods) || []
@@ -914,6 +984,7 @@ export default {
'is-picked': !hasLesson && this.isSelected(day.date, jc),
'has-lesson': hasLesson,
'is-conflict': !!(cell && !cell.unavailable && this.conflictHit(day, jc)),
'drop-hover': this.moveMode && this.dropTarget === this.cellKey(day.date, jc),
'day-off': !!day.unavailableReason
}
},
@@ -950,6 +1021,12 @@ export default {
this.$message.warning(cell.reason || '该节次不可排课')
return
}
// 拖拽调课模式下按住格子只做聚焦,课次块用 HTML5 拖拽移动
if (this.moveMode) {
this.focusCell(day.date, jc)
if (this.$refs.gridWrap && this.$refs.gridWrap.focus) this.$refs.gridWrap.focus()
return
}
this.dragging = true
this.additiveDrag = !!(ev && (ev.ctrlKey || ev.metaKey))
this.dragStart = { fi, jc }
@@ -1006,6 +1083,78 @@ export default {
clearSelection() {
this.selectedCells = []
},
/* ==================== 拖拽调课(E4 ==================== */
lessonEditable(l) {
const c = this.courses.find(x => x.sskcbh === l.sskcbh)
return !!(c && c.editable)
},
onLessonDragstart(l, day, jc, ev) {
if (!this.moveMode || !this.lessonEditable(l)) {
ev.preventDefault()
return
}
ev.dataTransfer.setData('text/plain', JSON.stringify({
sskcbh: l.sskcbh,
rq: this.fmtDate(day.date),
jc
}))
ev.dataTransfer.effectAllowed = 'move'
ev.stopPropagation()
},
onCellDragover(day, jc, ev) {
if (!this.moveMode) return
const cell = this.cellOf(day, jc)
if (cell && cell.unavailable) return
ev.preventDefault()
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move'
this.dropTarget = this.cellKey(day.date, jc)
},
onCellDragleave(day, jc) {
if (this.dropTarget === this.cellKey(day.date, jc)) this.dropTarget = null
},
onCellDrop(day, jc, ev) {
if (!this.moveMode) return
ev.preventDefault()
this.dropTarget = null
let payload = null
try {
payload = JSON.parse(ev.dataTransfer.getData('text/plain'))
} catch (e) {
return
}
if (!payload || !payload.sskcbh) return
const to = { rq: this.fmtDate(day.date), jc }
if (payload.rq === to.rq && payload.jc === to.jc) return
this.submitting = true
moveLesson({ sskcbh: payload.sskcbh, from: { rq: payload.rq, jc: payload.jc }, to })
.then(res => {
const data = (res && res.data) || {}
if (data.warning) {
this.$message.warning('已移动,但 ' + data.warning)
} else {
this.$message.success('课次已移动')
}
this.loadView()
})
.catch(err => {
this.$message.error((err && err.message) || '移动失败')
})
.finally(() => {
this.submitting = false
})
},
/* ==================== 导出 / 打印(E4 ==================== */
handleExport() {
if (!this.xydxqbh) return
exportSchedulingView(this.xydxqbh)
.then(blob => {
saveAs(blob, `排课窗口_${this.xydxqbh}.xlsx`)
})
.catch(() => this.$message.error('导出失败'))
},
handlePrint() {
window.print()
},
/** ⑨ 当前格子编排信息 */
focusCell(rq, jc) {
this.focusedCell = { rq: this.fmtDate(rq), jc }
@@ -1889,3 +2038,36 @@ export default {
padding: 0;
}
</style>
<style lang="scss">
/* E4:拖拽调课与打印(非 scoped,拖拽/打印需命中 body 级) */
.tb-slot.drop-hover {
outline: 2px dashed var(--edu-color-primary, #2f7d5c);
outline-offset: -2px;
background: var(--edu-color-primary-faint, #eef7f2);
}
.lesson.is-draggable {
cursor: grab;
}
.lesson.is-draggable:active {
cursor: grabbing;
}
@media print {
.page-head,
.op-bar,
.cell-info-bar,
.grid-controls,
.week-nav .el-radio-group,
.week-nav .el-button,
.el-dialog__wrapper,
.v-modal {
display: none !important;
}
.timetable-wrap {
max-height: none !important;
overflow: visible !important;
}
}
</style>
@@ -0,0 +1,137 @@
<template>
<div class="app-container">
<el-tabs v-model="tab" type="border-card">
<!-- ==================== 今日本室 ==================== -->
<el-tab-pane label="今日本室" name="today">
<div class="day-bar">
<el-button size="small" icon="el-icon-arrow-left" @click="shiftDay(-1)">上一天</el-button>
<el-date-picker
v-model="day"
type="date"
value-format="yyyy-MM-dd"
:clearable="false"
style="width: 150px"
size="small"
@change="loadToday"
/>
<el-button size="small" @click="shiftDay(1)">下一天<i class="el-icon-arrow-right" /></el-button>
</div>
<el-table v-loading="todayLoading" :data="todayRows" border stripe size="small" max-height="560">
<el-table-column prop="sksj" label="上课时间" width="170" align="center" />
<el-table-column prop="kc" label="课程" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="bc" label="班次" min-width="130" align="center" show-overflow-tooltip />
<el-table-column prop="jy" label="教员" width="140" align="center" />
<el-table-column prop="cd" label="场地" width="120" align="center" />
<el-table-column prop="jxnrxff" label="教学内容 / 方法" min-width="160" align="center" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<!-- ==================== 本室课程 ==================== -->
<el-tab-pane label="本室课程" name="courses">
<el-form inline size="small" style="margin-bottom: 8px">
<el-form-item label="年度">
<el-input-number v-model="courseQuery.nd" :min="2000" :max="2100" style="width: 120px" />
</el-form-item>
<el-form-item label="学期第次">
<el-input-number v-model="courseQuery.xqdc" :min="1" :max="4" style="width: 100px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadCourses">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="courseLoading" :data="courseRows" border stripe size="small" max-height="560">
<el-table-column prop="kcmcksxs" label="课程名称/课时系数" min-width="180" align="center" show-overflow-tooltip />
<el-table-column prop="zrjykc" label="责任教员/课次" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="jhxs" label="计划学时" width="80" align="center" />
<el-table-column prop="yxxs" label="运行学时" width="80" align="center" />
<el-table-column prop="khlxfs" label="考核类型/方式" width="120" align="center" />
<el-table-column prop="ssjy" label="实施教员" width="110" align="center" />
<el-table-column prop="rscd" label="人数/场地" min-width="140" align="center" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<!-- ==================== 本室教员 ==================== -->
<el-tab-pane label="本室教员" name="teachers">
<el-table v-loading="teacherLoading" :data="teacherRows" border stripe size="small" max-height="560">
<el-table-column prop="jybh" label="教员编号" width="140" align="center" />
<el-table-column prop="jyxm" label="姓名" min-width="140" align="center">
<template slot-scope="{ row }">
{{ row.jyxm || '-' }}<span v-if="row.zr === 1" class="dir-star" title="教研室主任"> *</span>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import { myOfficeToday, myOfficeCourses, myOfficeTeachers } from '@/api/teachBusiness/my'
export default {
name: 'MyOffice',
data() {
return {
tab: 'today',
day: '',
todayRows: [],
todayLoading: false,
courseQuery: { nd: null, xqdc: null },
courseRows: [],
courseLoading: false,
teacherRows: [],
teacherLoading: false
}
},
created() {
this.day = this.fmt(new Date())
this.loadToday()
this.loadCourses()
this.loadTeachers()
},
methods: {
fmt(d) {
const m = `${d.getMonth() + 1}`.padStart(2, '0')
const dd = `${d.getDate()}`.padStart(2, '0')
return `${d.getFullYear()}-${m}-${dd}`
},
shiftDay(n) {
const d = new Date(this.day)
d.setDate(d.getDate() + n)
this.day = this.fmt(d)
this.loadToday()
},
loadToday() {
this.todayLoading = true
myOfficeToday({ rq: this.day })
.then(res => { this.todayRows = res.data || [] })
.finally(() => { this.todayLoading = false })
},
loadCourses() {
this.courseLoading = true
myOfficeCourses(this.courseQuery)
.then(res => { this.courseRows = res.data || [] })
.finally(() => { this.courseLoading = false })
},
loadTeachers() {
this.teacherLoading = true
myOfficeTeachers()
.then(res => { this.teacherRows = res.data || [] })
.finally(() => { this.teacherLoading = false })
}
}
}
</script>
<style scoped>
.day-bar {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.dir-star {
color: #e6a23c;
font-weight: bold;
}
</style>
@@ -0,0 +1,158 @@
<template>
<div class="app-container">
<el-tabs v-model="tab" type="border-card">
<!-- ==================== 我的课程任务 ==================== -->
<el-tab-pane label="我的课程任务" name="courses">
<el-form inline size="small" style="margin-bottom: 8px">
<el-form-item label="年度">
<el-input-number v-model="courseQuery.nd" :min="2000" :max="2100" style="width: 120px" />
</el-form-item>
<el-form-item label="学期第次">
<el-input-number v-model="courseQuery.xqdc" :min="1" :max="4" style="width: 100px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadCourses">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="courseLoading" :data="courseRows" border stripe size="small" max-height="560">
<el-table-column prop="rwmc" label="教学任务" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="kcmc" label="课程" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="klx" label="课类型" width="80" align="center" />
<el-table-column prop="xs" label="学时" width="60" align="center" />
<el-table-column prop="xydmc" label="学员队" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="kcxh" label="课次" width="60" align="center" />
<el-table-column label="责任教员" width="110" align="center">
<template slot-scope="{ row }">
{{ row.jyxm || '-' }}<span v-if="row.zr === 1" class="dir-star" title="教研室主任"> *</span>
</template>
</el-table-column>
<el-table-column prop="jysjhjyxm" label="计划教员" width="100" align="center" />
<el-table-column label="场地" width="120" align="center" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.jsmc || row.jsbh || '-' }}</template>
</el-table-column>
<el-table-column label="合班" width="80" align="center">
<template slot-scope="{ row }">
<el-tag v-if="row.bz2" size="small">{{ row.bz2 }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- ==================== 我的课表 ==================== -->
<el-tab-pane label="我的课表" name="grid">
<el-form inline size="small" style="margin-bottom: 8px">
<el-form-item label="日期范围">
<el-date-picker
v-model="gridRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="~"
start-placeholder="开始"
end-placeholder="结束"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadGrid">生成</el-button>
</el-form-item>
</el-form>
<timetable-grid-view :grid="grid" :loading="gridLoading" :show-team="true" :show-room="true" :show-teacher="false" />
</el-tab-pane>
<!-- ==================== 实施计划 ==================== -->
<el-tab-pane label="我的实施计划" name="lessons">
<el-form inline size="small" style="margin-bottom: 8px">
<el-form-item label="日期范围">
<el-date-picker
v-model="lessonRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="~"
start-placeholder="开始"
end-placeholder="结束"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadLessons">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="lessonLoading" :data="lessonRows" border stripe size="small" max-height="560">
<el-table-column prop="sksj" label="上课时间" width="170" align="center" />
<el-table-column prop="kc" label="课程" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="bc" label="班次" min-width="130" align="center" show-overflow-tooltip />
<el-table-column prop="jy" label="教员" width="120" align="center" />
<el-table-column prop="cd" label="场地" width="120" align="center" />
<el-table-column prop="jxnrxff" label="教学内容 / 方法" min-width="160" align="center" show-overflow-tooltip />
<el-table-column prop="bz" label="备注" min-width="100" align="center" show-overflow-tooltip />
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import { myTeacherCourses, myTeacherLessons, myTeacherGrid } from '@/api/teachBusiness/my'
import TimetableGridView from '@/components/TimetableGridView'
export default {
name: 'MyTeach',
components: { TimetableGridView },
data() {
return {
tab: 'courses',
courseQuery: { nd: null, xqdc: null },
courseRows: [],
courseLoading: false,
gridRange: null,
grid: null,
gridLoading: false,
lessonRange: null,
lessonRows: [],
lessonLoading: false
}
},
created() {
this.loadCourses()
this.loadGrid()
},
methods: {
loadCourses() {
this.courseLoading = true
myTeacherCourses(this.courseQuery)
.then(res => { this.courseRows = res.data || [] })
.finally(() => { this.courseLoading = false })
},
loadGrid() {
this.gridLoading = true
const params = {}
if (this.gridRange && this.gridRange.length === 2) {
params.start = this.gridRange[0]
params.end = this.gridRange[1]
}
myTeacherGrid(params)
.then(res => { this.grid = res.data })
.finally(() => { this.gridLoading = false })
},
loadLessons() {
this.lessonLoading = true
const params = {}
if (this.lessonRange && this.lessonRange.length === 2) {
params.start = this.lessonRange[0]
params.end = this.lessonRange[1]
}
myTeacherLessons(params)
.then(res => { this.lessonRows = res.data || [] })
.finally(() => { this.lessonLoading = false })
}
}
}
</script>
<style scoped>
.dir-star {
color: #e6a23c;
font-weight: bold;
}
</style>
@@ -12,7 +12,8 @@
<el-button size="small" :disabled="!semesterBh" @click="handleApplyRegion">选定区域应用于其它班次</el-button>
<el-button size="small" :disabled="!semesterBh" @click="handleApplyWhole">整个班历应用于其它班次</el-button>
<el-button size="small" :disabled="!semesterBh" :loading="resyncLoading" @click="handleResync">同步校历</el-button>
<span class="hint">应用到其它班次只覆盖班历时间格不改课程任务同步校历只补缺失格不覆盖人工改动</span>
<el-checkbox v-model="resyncForce" size="small">覆盖已有格</el-checkbox>
<span class="hint">应用到其它班次只覆盖班历时间格不改课程任务勾选覆盖已有格与校历不一致的班历格子将被校历内容覆盖</span>
</div>
<div class="editor-wrap">
<SchoolCalendarEditor
@@ -57,7 +58,8 @@ export default {
return {
teamSelectVisible: false,
applyWhole: false,
resyncLoading: false
resyncLoading: false,
resyncForce: false
}
},
computed: {
@@ -137,22 +139,34 @@ export default {
},
handleResync() {
this.resyncLoading = true
resyncClassCalendar(this.semesterBh, false).then(res => {
const force = this.resyncForce
resyncClassCalendar(this.semesterBh, false, force).then(res => {
const data = res && res.data
if (!data || !data.addCount) {
const addCount = (data && data.addCount) || 0
const updateCount = (data && data.updateCount) || 0
if (!addCount && !updateCount) {
this.$message.info('班历已与校历一致,无需同步')
return
}
const lines = (data.items || []).slice(0, 20)
.map(i => `${i.jqsj} ${i.courseClass}${i.jqmc || ''}${i.kpk === false ? '(不可排课)' : ''}`)
const more = data.addCount > lines.length ? `<br/>…共 ${data.addCount}` : ''
this.$confirm(
`校历中有 ${data.addCount} 个班历缺失的时间格:<br/>${lines.join('<br/>')}${more}<br/><br/>补齐这些格子?(不覆盖已有班历内容)`,
'同步校历',
{ dangerouslyUseHTMLString: true, confirmButtonText: '补齐', cancelButtonText: '取消' }
).then(() => {
return resyncClassCalendar(this.semesterBh, true).then(r => {
this.$message.success(`已补齐 ${(r.data && r.data.addCount) || 0} 个时间格`)
const more = addCount > lines.length ? `<br/>…共 ${addCount}` : ''
let msg = ''
if (addCount) {
msg += `校历中有 ${addCount} 个班历缺失的时间格:<br/>${lines.join('<br/>')}${more}<br/>`
}
if (updateCount) {
msg += `<br/>另有 ${updateCount} 个已有格与校历不一致,将被<span style="color:#f56c6c">覆盖为校历内容</span>(人工改动丢失)。<br/>`
}
msg += '<br/>确认执行同步?'
this.$confirm(msg, '同步校历', {
dangerouslyUseHTMLString: true,
confirmButtonText: '同步',
cancelButtonText: '取消'
}).then(() => {
return resyncClassCalendar(this.semesterBh, true, force).then(r => {
const d = r.data || {}
this.$message.success(`已补齐 ${d.addCount || 0}${force ? `,覆盖 ${d.updateCount || 0}` : ''}`)
const editor = this.$refs.editor
if (editor && editor.reloadEvents) editor.reloadEvents()
})
@@ -22,24 +22,42 @@
</span>
</div>
<!-- 每周可排正课时间轴按周月刻度 -->
<!-- 每周可排正课时间轴按周/月刻度切换 -->
<div class="axis-toolbar">
<el-radio-group v-model="axisMode" size="mini">
<el-radio-button label="week">按周</el-radio-button>
<el-radio-button label="month">按月</el-radio-button>
</el-radio-group>
</div>
<div class="week-axis">
<table class="axis-table">
<thead>
<tr>
<th class="sticky-col first">周次</th>
<th v-for="w in view.weeks" :key="'wn' + w.weekNo" class="week-col"
:class="{ 'month-start': isMonthStart(w) }">
W{{ w.weekNo }}
<div class="week-date">{{ shortDate(w.startDate) }}</div>
<th class="sticky-col first">{{ axisMode === 'week' ? '周次' : '月份' }}</th>
<th v-for="col in axisCols" :key="'wn' + col.key" class="week-col"
:class="{ 'month-start': axisMode === 'week' && isMonthStart(col.weeks[0]) }">
{{ col.label }}
<div class="week-date">{{ col.sublabel }}</div>
</th>
</tr>
<tr>
<th class="sticky-col first">可排正课</th>
<th v-for="w in view.weeks" :key="'wh' + w.weekNo" class="week-col hours-cell"
:class="{ 'month-start': isMonthStart(w) }"
:title="sourceText(w.source)">
{{ w.availableHours }}h
<th v-for="col in axisCols" :key="'wh' + col.key" class="week-col hours-cell"
:class="{ 'month-start': axisMode === 'week' && isMonthStart(col.weeks[0]) }"
:title="col.weeks.map(w => sourceText(w.source)).join('、')">
{{ sumOf(col.weeks, 'availableHours') }}h
</th>
</tr>
<tr>
<th class="sticky-col first">已铺</th>
<th v-for="col in axisCols" :key="'ws' + col.key" class="week-col used-cell">
{{ sumOf(col.weeks, 'scheduledHours') }}h
</th>
</tr>
<tr>
<th class="sticky-col first">剩余</th>
<th v-for="col in axisCols" :key="'wr' + col.key" class="week-col remain-cell">
{{ sumOf(col.weeks, 'remainingHours') }}h
</th>
</tr>
</thead>
@@ -53,7 +71,12 @@
<div class="list-actions">
<el-button size="small" type="primary" :disabled="!selection.length || view.frozen"
@click="handleSaveSelected">保存所选</el-button>
<el-button size="small" type="warning" plain :disabled="view.frozen"
@click="handleAutoArrange(false)">自动排布</el-button>
<el-button size="small" type="warning" plain :disabled="view.frozen"
@click="handleAutoArrange(true)">辅助排布</el-button>
<el-button size="small" icon="el-icon-refresh" @click="loadData">刷新</el-button>
<el-button size="small" icon="el-icon-printer" @click="handlePrint">打印</el-button>
<el-button size="small" icon="el-icon-download" @click="handleExportImage">导出图像</el-button>
<el-button size="small" icon="el-icon-download" @click="handleExportExcel">导出 Excel</el-button>
</div>
@@ -71,14 +94,16 @@
<th class="sticky-col c-name">课程</th>
<th class="sticky-col c-num">学时</th>
<th class="sticky-col c-num">周课时</th>
<th class="sticky-col c-num" title="实施课程表已排课次折算学时">已排</th>
<th class="sticky-col c-num" title="学时 已排学时">剩余</th>
<th class="sticky-col c-num" title="优选序数">序数</th>
<th class="sticky-col c-num" title="配档起始周">起周</th>
<th class="sticky-col c-mode">连排/按周</th>
<th class="sticky-col c-num" title="配档编组(0=不编组)">编组</th>
<th class="sticky-col c-ops">操作</th>
<th v-for="w in view.weeks" :key="'tc' + w.weekNo"
class="week-col" :class="{ 'month-start': isMonthStart(w) }">
{{ w.weekNo }}
<th v-for="col in axisCols" :key="'tc' + col.key"
class="week-col" :class="{ 'month-start': axisMode === 'week' && isMonthStart(col.weeks[0]) }">
{{ col.short }}
</th>
</tr>
</thead>
@@ -91,6 +116,8 @@
<td class="sticky-col c-name" :title="row.kbh">{{ row.kcmc }}</td>
<td class="sticky-col c-num">{{ row.xs }}</td>
<td class="sticky-col c-num">{{ row.zks }}</td>
<td class="sticky-col c-num used-cell">{{ row.scheduledHours == null ? '-' : row.scheduledHours }}</td>
<td class="sticky-col c-num remain-cell">{{ row.remainingHours == null ? '-' : row.remainingHours }}</td>
<td class="sticky-col c-num">
<el-input-number v-model="row.pdxh" :min="1" :controls="false" size="mini"
class="tiny-input" :disabled="view.frozen" />
@@ -117,13 +144,13 @@
<el-button type="text" size="mini" :disabled="view.frozen" @click="handleMove(row, 'down')"></el-button>
<el-button type="text" size="mini" @click="openGroupPanel(row)">编组</el-button>
</td>
<td v-for="w in view.weeks" :key="'cell' + row.bh + w.weekNo"
class="week-col cell" :class="cellClass(row, w)">
{{ cellHours(row, w.weekNo) || '' }}
<td v-for="col in axisCols" :key="'cell' + row.bh + col.key"
class="week-col cell" :class="cellClassOf(row, col)">
{{ cellHoursOf(row, col) || '' }}
</td>
</tr>
<tr v-if="!view.tasks.length">
<td :colspan="10 + view.weeks.length" class="empty-row">
<td :colspan="12 + axisCols.length" class="empty-row">
该班次学期暂无课程任务请先在班次教学任务中生成/添加课程
</td>
</tr>
@@ -172,7 +199,8 @@ import {
allocationMove,
allocationGroups,
allocationSetGroup,
allocationExport
allocationExport,
allocationAutoArrange
} from '@/api/teachBusiness/allocation'
const SOURCE_TEXT = { class: '班历', school: '校历', default: '默认(30学时)' }
@@ -187,6 +215,7 @@ export default {
data() {
return {
loading: false,
axisMode: 'week',
view: { weeks: [], tasks: [], totalWeeks: 0, frozen: false, ndCode: '', kxrq: '', jsrq: '', xydmc: '' },
selection: [],
groupPanelVisible: false,
@@ -201,6 +230,38 @@ export default {
semesterName() {
const s = this.semester || {}
return s.xydmc || s.xydbh || ''
},
/**
* 轴列:week=每周一列;month=按周起所在月份归并(label=月名,sublabel=覆盖周次)
*/
axisCols() {
const weeks = this.view.weeks || []
if (this.axisMode === 'week') {
return weeks.map(w => ({
key: 'w' + w.weekNo,
label: 'W' + w.weekNo,
short: w.weekNo,
sublabel: this.shortDate(w.startDate),
weeks: [w]
}))
}
const groups = []
const byMonth = {}
weeks.forEach(w => {
const d = w.startDate ? new Date(String(w.startDate).replace(/-/g, '/')) : null
const key = d && !isNaN(d) ? d.getFullYear() + '-' + (d.getMonth() + 1) : '其它'
if (!byMonth[key]) {
byMonth[key] = { key: 'm' + key, label: key === '其它' ? '其它' : (d.getMonth() + 1) + '月', short: key === '其它' ? '?' : (d.getMonth() + 1) + '月', sublabel: '', weeks: [] }
groups.push(byMonth[key])
}
byMonth[key].weeks.push(w)
})
groups.forEach(g => {
const first = g.weeks[0].weekNo
const last = g.weeks[g.weeks.length - 1].weekNo
g.sublabel = 'W' + first + (last > first ? '-' + last : '')
})
return groups
}
},
watch: {
@@ -303,16 +364,46 @@ export default {
})
},
/* ---------- 自动排布 ---------- */
handleAutoArrange(onlyMissing) {
const s = this.semester || {}
if (!s.bh) return
const selected = this.selection.length ? this.selection.map(r => r.bh) : null
const scopeText = selected ? `所选 ${selected.length} 条任务` : '全部任务'
const modeText = onlyMissing
? '只回填尚未设置起始周的任务(不动人工排布)'
: '按铺学时建议重算并覆盖全部起始周'
this.$confirm(`将对${scopeText}执行「${onlyMissing ? '辅助排布' : '自动排布'}」:${modeText},配档起始周写库。是否继续?`, '自动排布', {
type: 'warning'
}).then(() => {
return allocationAutoArrange(s.bh, onlyMissing, selected)
}).then(res => {
this.$message.success(`已回填 ${res && res.data || 0} 条任务的起始周`)
this.loadData()
}).catch(() => {})
},
/* ---------- 打印 ---------- */
handlePrint() {
window.print()
},
/* ---------- 单元格 ---------- */
sumOf(weeks, field) {
return (weeks || []).reduce((sum, w) => sum + (w[field] || 0), 0)
},
cellHours(row, weekNo) {
const hit = (row.distribution || []).find(item => item.week === weekNo)
return hit ? hit.hours : 0
},
cellClass(row, w) {
const hours = this.cellHours(row, w.weekNo)
cellHoursOf(row, col) {
return col.weeks.reduce((sum, w) => sum + this.cellHours(row, w.weekNo), 0)
},
cellClassOf(row, col) {
const hours = this.cellHoursOf(row, col)
return {
'cell-active': hours > 0,
'cell-overflow-week': hours > w.availableHours
'cell-overflow-week': hours > this.sumOf(col.weeks, 'availableHours')
}
},
isMonthStart(w) {
@@ -418,6 +509,9 @@ export default {
.week-col { min-width: 34px; }
.month-start { border-left: 2px solid #dcdfe6 !important; }
.hours-cell { color: #409eff; font-weight: 600; }
.used-cell { color: #67c23a; font-weight: 600; }
.remain-cell { color: #e6a23c; font-weight: 600; }
.axis-toolbar { margin: 4px 0; text-align: right; }
.alloc-table-wrap { overflow: auto; max-height: 52vh; border: 1px solid #ebeef5; }
.alloc-table {
@@ -427,7 +521,7 @@ export default {
.c-name { left: 72px; min-width: 120px; max-width: 160px; text-align: left; }
.c-num { min-width: 56px; }
.c-mode { min-width: 74px; }
.c-ops { left: 486px; min-width: 150px; white-space: nowrap; }
.c-ops { left: 598px; min-width: 150px; white-space: nowrap; }
.cell { min-width: 34px; color: #909399; }
.cell-active { background: #d9ecff; color: #1f5faa; font-weight: 600; }
.cell-overflow-week { background: #fde2e2; }
@@ -450,3 +544,23 @@ export default {
}
}
</style>
<!-- 打印只输出配当内容区 -->
<style>
@media print {
body * { visibility: hidden; }
.allocation-page, .allocation-page * { visibility: visible; }
.allocation-page {
position: absolute;
left: 0;
top: 0;
width: 100%;
padding: 0;
}
.allocation-page .list-actions,
.allocation-page .c-ops,
.allocation-page .axis-toolbar,
.allocation-page .el-checkbox { display: none !important; }
.allocation-page .alloc-table-wrap { max-height: none !important; overflow: visible !important; }
}
</style>
@@ -64,6 +64,7 @@
<el-button type="success" plain icon="el-icon-date" :disabled="!currentRow" @click="handleEditEventCalendar">编辑班历</el-button>
<el-button type="success" plain icon="el-icon-calendar" :disabled="!currentRow" @click="handleEditCalendar">编辑班次教学任务</el-button>
<el-button type="success" plain icon="el-icon-data-line" :disabled="!currentRow" @click="handleAllocation">教学配当</el-button>
<el-button type="success" plain icon="el-icon-download" :disabled="!selection.length" @click="handleExportAllocation">导出配当</el-button>
<el-button type="primary" plain icon="el-icon-upload2" :disabled="!selection.length" @click="handlePublishRunning">发布运行课表</el-button>
<el-button type="danger" plain icon="el-icon-delete-solid" :disabled="!selection.length" @click="handleWithdrawRunning">删除运行课表</el-button>
</div>
@@ -335,6 +336,7 @@ import { batchAutoGenerateTasks } from '@/api/studentRecords/classCalendar'
import { listSemester, allSemester, addSemester, updateSemester, delSemester, batchDeleteSemester, batchUpdateDateRange, batchUpdateJxrwbh, batchCreateSemester, batchUpdateXqdc, batchUpdateKfjypk, batchWaveMerge, batchWaveSplit, listCreatableClasses, presetSemesterDates } from '@/api/studentRecords/semester'
import { listAllSemester } from '@/api/teachBusiness/semester'
import { listTeachingTask } from '@/api/teachBusiness/teachingTask'
import { allocationExport } from '@/api/teachBusiness/allocation'
import { publishRunningCourses, withdrawRunningCourses } from '@/api/teachBusiness/schedulingWindow'
export default {
@@ -668,6 +670,23 @@ export default {
this.allocationVisible = true
},
// 导出配当(多学期批量):勾选几个班次学期就导出几个
handleExportAllocation() {
const ids = this.selection.map(item => item.bh || item.xydxqbh).filter(Boolean)
if (!ids.length) {
this.$message.warning('请先勾选要导出的班次学期')
return
}
allocationExport(ids).then(blob => {
const url = window.URL.createObjectURL(new Blob([blob]))
const link = document.createElement('a')
link.href = url
link.download = `教学配当_${ids.length}个班次学期.xlsx`
link.click()
window.URL.revokeObjectURL(url)
})
},
// 批量发布【运行课表】(手册 12.1):把课程任务发布为运行课程,之后才能在排课窗口排课
handlePublishRunning() {
const rows = this.selection.slice()
@@ -31,7 +31,34 @@
:disabled="frozen || !selection.length"
@click="handleSplit"
>拆班</el-button>
<el-button
size="small"
icon="el-icon-user"
:disabled="frozen || !selection.length"
@click="openBatchTeacher"
>批量指定教员{{ selection.length }}</el-button>
<el-button
size="small"
icon="el-icon-office-building"
:disabled="frozen || !selection.length"
@click="openBatchRoom"
>批量指定场地{{ selection.length }}</el-button>
<el-button
size="small"
icon="el-icon-edit-outline"
:disabled="frozen || !selection.length"
@click="openBatchFill"
>批量填报{{ selection.length }}</el-button>
<el-button
type="danger"
size="small"
icon="el-icon-delete"
:disabled="frozen || !selection.length"
@click="handleDeleteRows"
>删除行{{ selection.length }}</el-button>
<el-button size="small" icon="el-icon-refresh" @click="loadRows">刷新</el-button>
<el-button size="small" icon="el-icon-download" @click="handleExport">导出</el-button>
<el-button size="small" icon="el-icon-printer" @click="handlePrint">打印</el-button>
<span v-if="frozen" class="tb-frozen-tip">教学任务未发布或已结束当前只读</span>
<span class="tb-tip">合班规则科目学时课类型成绩分制必须相同同合班行同色连显</span>
</div>
@@ -57,7 +84,9 @@
<el-table-column prop="zks" label="周课时" width="65" align="center" />
<el-table-column prop="cjfz" label="成绩分制" width="80" align="center" />
<el-table-column label="责任教员" width="110" align="center">
<template slot-scope="{ row }">{{ row.jyxm || '-' }}</template>
<template slot-scope="{ row }">
{{ row.jyxm || '-' }}<span v-if="row.zr === 1" class="tb-director" title="教研室主任"> *</span>
</template>
</el-table-column>
<el-table-column label="场地" width="130" align="center" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.jsmc || row.jsbh || '-' }}</template>
@@ -76,11 +105,12 @@
<template slot-scope="{ row }">{{ row.jysjhjyxm || '-' }}</template>
</el-table-column>
<el-table-column prop="jysjhbz" label="排课建议" min-width="120" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="210" align="center" fixed="right">
<el-table-column label="操作" width="270" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" :disabled="frozen" @click="openTeacher(row)">指定教员</el-button>
<el-button type="text" size="small" :disabled="frozen" @click="openRoom(row)">指定场地</el-button>
<el-button type="text" size="small" :disabled="frozen" @click="openFill(row)">填报</el-button>
<el-button type="text" size="small" style="color:#f56c6c" :disabled="frozen" @click="handleDeleteRows([row])">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -98,6 +128,9 @@
:close-on-click-modal="false"
>
<el-form label-width="110px">
<el-form-item v-if="teacherDialog.batch" label="选中行">
<span>{{ selection.length }} 行将统一指定责任教员</span>
</el-form-item>
<el-form-item label="指定方式">
<el-radio-group v-model="teacherDialog.mode" @change="loadTeacherOptions">
<el-radio label="unit">责任单位教员</el-radio>
@@ -122,7 +155,7 @@
</el-select>
</el-form-item>
<el-form-item v-else label="计划教员">
<span>{{ teacherDialog.planTeacherName || '(该行没有教研室计划教员)' }}</span>
<span>{{ teacherDialog.batch ? '应用各行教研室计划教员' : (teacherDialog.planTeacherName || '(该行没有教研室计划教员)') }}</span>
</el-form-item>
<el-form-item v-if="teacherDialog.row && teacherDialog.row.bz2 && teacherDialog.row.bz2 !== 0" label="同步范围">
<span class="tb-tip">该行已合班(组{{ teacherDialog.row.bz2 }}),保存后同组其它行同步</span>
@@ -143,6 +176,9 @@
:close-on-click-modal="false"
>
<el-form label-width="110px">
<el-form-item v-if="roomDialog.batch" label="选中行">
<span>{{ selection.length }} 行将统一指定场地</span>
</el-form-item>
<el-form-item label="场地类型">
<el-radio-group v-model="roomDialog.useSpecial">
<el-radio :label="true">班次专用教室</el-radio>
@@ -178,13 +214,16 @@
<!-- ==================== 填报子对话框 ==================== -->
<el-dialog
:visible.sync="fillDialog.visible"
title="教研室填报"
:title="fillDialog.batch ? `批量填报(${selection.length} 行)` : '教研室填报'"
width="520px"
append-to-body
:close-on-click-modal="false"
>
<el-form label-width="110px">
<el-form-item label="课程 / 学员队">
<el-form-item v-if="fillDialog.batch" label="选中行">
<span>{{ selection.length }} 行将统一应用下方填写的字段(留空字段不改动)</span>
</el-form-item>
<el-form-item v-else label="课程 / 学员队">
<span>{{ fillDialog.row ? `${fillDialog.row.kcmc} / ${fillDialog.row.xydmc}` : '' }}</span>
</el-form-item>
<el-form-item label="计划教员">
@@ -223,6 +262,15 @@
/>
</el-select>
</el-form-item>
<el-form-item label="课次序号">
<el-input-number
v-model="fillDialog.kcxh"
:min="1"
:max="999"
placeholder="课序"
style="width: 160px"
/>
</el-form-item>
<el-form-item label="排课建议">
<el-input
v-model="fillDialog.jysjhbz"
@@ -253,8 +301,13 @@ import {
taskBookSplit,
taskBookSetTeacher,
taskBookSetRoom,
taskBookBatchSetTeacher,
taskBookBatchSetRoom,
taskBookBatchFill,
taskBookDeleteRows,
taskBookFill
} from '@/api/teachBusiness/taskBook'
import { exportTaskBook } from '@/api/teachBusiness/teachingTask'
import { listTeacher } from '@/api/teachOffice/teacher'
import { listClassroom } from '@/api/teachBusiness/classroom'
@@ -275,6 +328,7 @@ export default {
selection: [],
teacherDialog: {
visible: false,
batch: false,
mode: 'unit',
jybh: '',
teachers: [],
@@ -283,6 +337,7 @@ export default {
},
roomDialog: {
visible: false,
batch: false,
useSpecial: true,
jsbh: '',
classrooms: [],
@@ -291,9 +346,11 @@ export default {
},
fillDialog: {
visible: false,
batch: false,
jysjhjybh: '',
jsbh: '',
jysjhbz: '',
kcxh: null,
useSpecial: false,
teachers: [],
classrooms: [],
@@ -369,15 +426,25 @@ export default {
// ---------- 指定教员 ----------
openTeacher(row) {
this.teacherDialog.row = row
this.teacherDialog.batch = false
this.teacherDialog.mode = 'unit'
this.teacherDialog.jybh = row.jybh || ''
this.teacherDialog.visible = true
this.loadTeacherOptions()
},
openBatchTeacher() {
this.teacherDialog.row = null
this.teacherDialog.batch = true
this.teacherDialog.mode = 'academy'
this.teacherDialog.jybh = ''
this.teacherDialog.visible = true
this.loadTeacherOptions()
},
loadTeacherOptions() {
const row = this.teacherDialog.row
if (!row) return
const params = this.teacherDialog.mode === 'unit' ? { jysdh: row.jysdh } : {}
const params = (!this.teacherDialog.batch && row && this.teacherDialog.mode === 'unit')
? { jysdh: row.jysdh }
: {}
this.teacherDialog.loading = true
listTeacher(params)
.then(res => {
@@ -389,7 +456,14 @@ export default {
},
submitTeacher() {
const d = this.teacherDialog
taskBookSetTeacher({ bh: d.row.bh, mode: d.mode, jybh: d.mode === 'plan' ? undefined : d.jybh })
const call = d.batch
? taskBookBatchSetTeacher({
bhList: this.selection.map(r => r.bh),
mode: d.mode,
jybh: d.mode === 'plan' ? undefined : d.jybh
})
: taskBookSetTeacher({ bh: d.row.bh, mode: d.mode, jybh: d.mode === 'plan' ? undefined : d.jybh })
call
.then(res => {
d.visible = false
this.reload(Promise.resolve(res))
@@ -399,9 +473,21 @@ export default {
// ---------- 指定场地 ----------
openRoom(row) {
this.roomDialog.row = row
this.roomDialog.batch = false
this.roomDialog.useSpecial = true
this.roomDialog.jsbh = ''
this.roomDialog.visible = true
this.loadRoomOptions()
},
openBatchRoom() {
this.roomDialog.row = null
this.roomDialog.batch = true
this.roomDialog.useSpecial = false
this.roomDialog.jsbh = ''
this.roomDialog.visible = true
this.loadRoomOptions()
},
loadRoomOptions() {
if (!this.roomDialog.classrooms.length) {
this.roomDialog.loading = true
listClassroom({ pageNum: 1, pageSize: 200 })
@@ -416,7 +502,14 @@ export default {
},
submitRoom() {
const d = this.roomDialog
taskBookSetRoom({ bh: d.row.bh, jsbh: d.jsbh, useSpecial: d.useSpecial })
const call = d.batch
? taskBookBatchSetRoom({
bhList: this.selection.map(r => r.bh),
jsbh: d.jsbh,
useSpecial: d.useSpecial
})
: taskBookSetRoom({ bh: d.row.bh, jsbh: d.jsbh, useSpecial: d.useSpecial })
call
.then(res => {
d.visible = false
this.reload(Promise.resolve(res))
@@ -426,9 +519,11 @@ export default {
// ---------- 填报 ----------
openFill(row) {
this.fillDialog.row = row
this.fillDialog.batch = false
this.fillDialog.jysjhjybh = row.jysjhjybh || ''
this.fillDialog.jsbh = row.jsbh || ''
this.fillDialog.jysjhbz = row.jysjhbz || ''
this.fillDialog.kcxh = row.kcxh || null
this.fillDialog.useSpecial = false
this.fillDialog.visible = true
this.fillDialog.loading = true
@@ -450,12 +545,57 @@ export default {
this.fillDialog.loading = false
})
},
openBatchFill() {
this.fillDialog.row = null
this.fillDialog.batch = true
this.fillDialog.jysjhjybh = ''
this.fillDialog.jsbh = ''
this.fillDialog.jysjhbz = ''
this.fillDialog.kcxh = null
this.fillDialog.useSpecial = false
this.fillDialog.visible = true
this.fillDialog.loading = true
Promise.all([
listTeacher({}).catch(() => null),
this.roomDialog.classrooms.length
? Promise.resolve(null)
: listClassroom({ pageNum: 1, pageSize: 200 }).catch(() => null)
])
.then(([tRes, cRes]) => {
if (tRes) this.fillDialog.teachers = (tRes.data && (tRes.data.records || tRes.data.rows || tRes.data)) || []
if (cRes) {
const data = cRes.data || {}
this.roomDialog.classrooms = data.records || data.rows || data.list || []
}
this.fillDialog.classrooms = this.roomDialog.classrooms
})
.finally(() => {
this.fillDialog.loading = false
})
},
submitFill() {
const d = this.fillDialog
if (d.batch) {
taskBookBatchFill({
bhList: this.selection.map(r => r.bh),
jysjhjybh: d.jysjhjybh || undefined,
jysjhbz: d.jysjhbz || undefined,
kcxh: d.kcxh || undefined,
jsbh: d.useSpecial ? undefined : (d.jsbh || undefined),
useSpecial: d.useSpecial
})
.then(res => {
d.visible = false
this.reload(Promise.resolve(res))
})
.catch(err => this.$message.error((err && err.message) || '保存失败'))
return
}
const base = {
bh: d.row.bh,
jysjhjybh: d.jysjhjybh || '',
jysjhbz: d.jysjhbz || ''
jysjhbz: d.jysjhbz || '',
kcxh: d.kcxh || undefined
}
// 专用教室走 setRoom(useSpecial) 才能取到班次专用教室编号;其它教室随 fill 同步
const call = d.useSpecial
@@ -467,6 +607,36 @@ export default {
this.reload(Promise.resolve(res))
})
.catch(err => this.$message.error((err && err.message) || '保存失败'))
},
// ---------- 行级删除 / 导出 / 打印 ----------
handleDeleteRows(rowsArg) {
const targets = Array.isArray(rowsArg) ? rowsArg : this.selection
const bhList = targets.map(r => r.bh)
if (!bhList.length) {
this.$message.warning('请先选择要删除的行')
return
}
this.$confirm(`确定删除选中的 ${bhList.length} 条课程任务行吗?已发布到运行课表的须先撤回。`, '删除课程任务行', {
type: 'warning'
})
.then(() => this.reload(taskBookDeleteRows(bhList)))
.catch(() => {})
},
handleExport() {
if (!this.jxrwbh) return
exportTaskBook(this.jxrwbh)
.then(res => {
const blob = new Blob([res.data || res], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `教学任务书_${this.taskName || this.jxrwbh}.xls`
link.click()
URL.revokeObjectURL(link.href)
})
.catch(() => this.$message.error('导出失败'))
},
handlePrint() {
window.print()
}
}
}
@@ -499,4 +669,29 @@ export default {
color: #c0c4cc;
font-size: 12px;
}
.tb-director {
color: #e6a23c;
font-weight: bold;
}
</style>
<style>
@media print {
.tb-toolbar,
.el-dialog__headerbtn,
.el-dialog__footer,
.tb-table .el-table__fixed-right,
.v-modal {
display: none !important;
}
.el-dialog {
width: 100% !important;
margin: 0 !important;
box-shadow: none !important;
}
.el-dialog__body {
max-height: none !important;
overflow: visible !important;
}
}
</style>
@@ -40,15 +40,38 @@
<div class="list-toolbar">
<div class="left-group">
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新建教学任务</el-button>
<el-button
type="success"
icon="el-icon-position"
:disabled="!selection.length"
@click="handleBatchPublish"
>批量发布{{ selection.length }}</el-button>
<el-button
type="danger"
icon="el-icon-delete"
:disabled="!selection.length"
@click="handleBatchDelete"
>批量删除{{ selection.length }}</el-button>
</div>
<div class="right-group">
<el-button icon="el-icon-collection" @click="openOfficeSummary">教研室任务书汇总</el-button>
</div>
</div>
<!-- ==================== 3. 数据表格 ==================== -->
<el-card shadow="never" class="table-card">
<el-table v-loading="loading" :data="tableData" border stripe class="task-table">
<el-table
v-loading="loading"
:data="tableData"
border
stripe
class="task-table"
@selection-change="val => selection = val"
>
<template slot="empty">
<span>无数据!</span>
</template>
<el-table-column type="selection" width="45" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="rwmc" label="任务名称" width="250" align="center" show-overflow-tooltip />
<el-table-column prop="nd" label="年度" width="150" align="center" />
@@ -195,6 +218,52 @@
:task-name="taskBookRow.rwmc"
:task-status="taskBookRow.zt"
/>
<!-- ==================== 7. 教研室任务书汇总对话框 ==================== -->
<el-dialog
:visible.sync="summaryVisible"
title="教研室任务书汇总"
width="860px"
:close-on-click-modal="false"
>
<el-form :inline="true">
<el-form-item label="教研室">
<el-select
v-model="summaryJysdh"
filterable
placeholder="请选择教研室"
style="width: 280px"
:loading="summaryOfficeLoading"
>
<el-option
v-for="o in officeOptions"
:key="o.jysdh"
:label="`${o.jysmc || o.jysdh}${o.jysdh}`"
:value="o.jysdh"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="!summaryJysdh" @click="loadOfficeSummary">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="summaryLoading" :data="summaryRows" border stripe max-height="460">
<template slot="empty"><span>无数据!</span></template>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="rwmc" label="教学任务" min-width="180" align="center" show-overflow-tooltip />
<el-table-column prop="nd" label="年度" width="90" align="center" />
<el-table-column label="任务状态" width="100" align="center">
<template slot-scope="{ row }">
<el-tag :type="taskTagType(row.taskZt)" size="small">{{ taskStatusLabel(row.taskZt) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="zt" label="任务书状态" width="110" align="center" />
<el-table-column prop="sbsj" label="上报时间" width="170" align="center" :formatter="fmtDateTime" />
</el-table>
<div slot="footer">
<el-button @click="summaryVisible = false">关 闭</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -212,10 +281,13 @@ import {
addTeachingTask,
updateTeachingTask,
deleteTeachingTask,
batchDeleteTeachingTask,
publishTeachingTask,
endPublishTeachingTask
} from '@/api/teachBusiness/teachingTask'
import { listAllSemester } from '@/api/teachBusiness/semester'
import { listOffice } from '@/api/teachOffice/office'
import { taskBookOfficeSummary } from '@/api/teachBusiness/taskBook'
import TaskBookFillDialog from './TaskBookFillDialog.vue'
export default {
@@ -244,6 +316,7 @@ export default {
// ==================== 2. 表格数据 ====================
loading: false,
tableData: [],
selection: [],
pageNum: 1,
pageSize: 10,
total: 0,
@@ -267,7 +340,15 @@ export default {
// ==================== 5. 任务书填报(阶段 4 ====================
taskBookVisible: false,
taskBookRow: {}
taskBookRow: {},
// ==================== 6. 教研室任务书汇总 ====================
summaryVisible: false,
summaryLoading: false,
summaryOfficeLoading: false,
summaryJysdh: '',
officeOptions: [],
summaryRows: []
}
},
created() {
@@ -452,6 +533,19 @@ export default {
this.fetchList()
}).catch(() => {})
},
/* ---------- 批量删除 ---------- */
handleBatchDelete() {
const bhList = this.selection.map(r => r.bh)
if (!bhList.length) return
this.$confirm(`确定要删除选中的 ${bhList.length} 个教学任务吗?删除将级联删除关联的教研室任务书。`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => batchDeleteTeachingTask(bhList)).then(() => {
this.$message.success('批量删除成功')
this.fetchList()
}).catch(() => {})
},
isTaskPublished(row) {
return row && (row.zt === '发布' || row.zt === '已发布')
@@ -483,11 +577,49 @@ export default {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => publishTeachingTask(row.bh)).then(() => {
}).then(() => publishTeachingTask([row.bh])).then(() => {
this.$message.success('发布成功')
this.fetchList()
}).catch(() => {})
},
/* ---------- 批量发布 ---------- */
handleBatchPublish() {
const bhList = this.selection.map(r => r.bh)
if (!bhList.length) return
this.$confirm(`确定要发布选中的 ${bhList.length} 个教学任务吗?未填写任务书的任务将被拒绝发布。`, '系统提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => publishTeachingTask(bhList)).then(res => {
this.$message.success(`成功发布 ${res.data || 0} 个教学任务`)
this.fetchList()
}).catch(() => {})
},
/* ---------- 教研室任务书汇总 ---------- */
openOfficeSummary() {
this.summaryVisible = true
this.summaryRows = []
if (!this.officeOptions.length) {
this.summaryOfficeLoading = true
listOffice({ pageNum: 1, pageSize: 500 }).then(res => {
const data = res.data || {}
this.officeOptions = data.records || data.rows || data.list || []
}).catch(() => {}).finally(() => {
this.summaryOfficeLoading = false
})
}
},
loadOfficeSummary() {
if (!this.summaryJysdh) return
this.summaryLoading = true
taskBookOfficeSummary(this.summaryJysdh).then(res => {
this.summaryRows = res.data || []
}).catch(() => {
this.summaryRows = []
}).finally(() => {
this.summaryLoading = false
})
},
handleEndPublish(row) {
this.$confirm(`确定要结束发布教学任务「${row.rwmc || row.bh || '该记录'}」吗?结束后任务书和班次课程任务将只读。`, '系统提示', {
confirmButtonText: '确定',