1、正课与学期关系关联;
2、天假删除保护; 3、教学任务计划增加了「结束发布」。已发布或库里的「已发布」都可以结束;结束后写入结束时间,任务书和班次课程任务只读 4、班次学期操作内容调整修改;
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible="dialogVisible"
|
||||
:title="`班历管理——${semesterTitle}`"
|
||||
:title="`班次教学任务——${semesterTitle}`"
|
||||
width="1200px"
|
||||
top="6vh"
|
||||
:close-on-click-modal="false"
|
||||
@@ -17,19 +17,23 @@
|
||||
</div>
|
||||
<div class="date-range-row">
|
||||
<div class="range-item">学期日期:{{ fmtDate(safeSemester.kxrq) }} 至 {{ fmtDate(safeSemester.jsrq) }},共 {{ semesterWeeks }} 周</div>
|
||||
<div class="range-item">班历记录:{{ records.length }} 条,总学时 {{ summary.totalXs }},总学分 {{ summary.totalXf }}</div>
|
||||
<div class="range-item">课程任务:{{ records.length }} 条,总学时 {{ summary.totalXs }},总学分 {{ summary.totalXf }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 2. 操作按钮区域 ==================== -->
|
||||
<div class="settings-panel">
|
||||
<div class="action-buttons-row">
|
||||
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增记录</el-button>
|
||||
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增课程任务</el-button>
|
||||
<el-button size="small" @click="openLibraryPicker">计划外新添</el-button>
|
||||
<el-button size="small" @click="openPlanPicker">计划内必修添加</el-button>
|
||||
<el-button size="small" @click="handleAutoGenerate">自动生成必修课程</el-button>
|
||||
<el-button size="small" @click="handlePresetMerge">预设合班</el-button>
|
||||
<el-button size="small" @click="handlePresetSplit">预设拆班</el-button>
|
||||
<el-button size="small" :disabled="!selection.length" @click="handleCopySelected">复制所选</el-button>
|
||||
<el-button size="small" :disabled="!clipboardCount" @click="handlePaste">粘贴到其它班次</el-button>
|
||||
<el-button size="small" :disabled="!selection.length" @click="handleSyncPlan">同步人培</el-button>
|
||||
<el-button size="small" type="danger" plain :disabled="!selection.length" @click="handleBatchDelete">批量删除</el-button>
|
||||
<el-button size="small" @click="handleApplyRegion">选定区域应用于其它班次</el-button>
|
||||
<el-button size="small" @click="handleApplyWhole">整个班历应用于其它班次</el-button>
|
||||
<el-button size="small" @click="handleApplyWhole">整个班次教学任务应用于其它班次</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,6 +48,7 @@
|
||||
size="small"
|
||||
max-height="480"
|
||||
class="calendar-table"
|
||||
:row-class-name="rowClassName"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
@@ -63,6 +68,11 @@
|
||||
<el-table-column prop="rs" label="人数" width="60" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="ksbh" label="考试编号" width="100" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="ksdd" label="考试地点" width="110" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="人培差异" width="90" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="isPlanMismatch(row)" type="danger" size="mini">不一致</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bz" label="备注" min-width="120" align="left" header-align="center" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
@@ -72,7 +82,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="select-tip">
|
||||
提示:勾选多条记录后可进行 预设合班 / 预设拆班 / 选定区域应用于其它班次 操作;删除为物理删除,请谨慎操作。
|
||||
提示:粉红行为与人培不一致,可用「同步人培」按人培覆盖。已发布到任务书或已排课的行不能删除。复制后粘贴会生成独立任务。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,6 +314,55 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:visible="libraryVisible"
|
||||
title="从课程库选择"
|
||||
width="760px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@update:visible="val => libraryVisible = val"
|
||||
>
|
||||
<el-input v-model="libraryKeyword" size="small" placeholder="简称 / 课程名称" clearable style="width: 240px; margin-bottom: 8px" @keyup.enter.native="loadLibrary" />
|
||||
<el-button size="small" type="primary" @click="loadLibrary">查询</el-button>
|
||||
<el-table v-loading="pickerLoading" :data="libraryRows" border size="small" max-height="360" highlight-current-row @current-change="row => libraryCurrent = row">
|
||||
<el-table-column prop="kbh" label="课编号" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="jc" label="简称" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="kmc" label="课程名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="xs" label="学时" width="70" align="center" />
|
||||
<el-table-column prop="klx" label="课类型" width="90" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div slot="footer">
|
||||
<el-button size="small" @click="libraryVisible = false">取消</el-button>
|
||||
<el-button size="small" type="primary" :disabled="!libraryCurrent" @click="confirmLibrary">加入任务</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:visible="planVisible"
|
||||
title="其它学期人培课程"
|
||||
width="760px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@update:visible="val => planVisible = val"
|
||||
>
|
||||
<el-table v-loading="pickerLoading" :data="planRows" border size="small" max-height="360" highlight-current-row @current-change="row => planCurrent = row">
|
||||
<el-table-column prop="jc" label="简称" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="kbh" label="课编号" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="xqdc" label="学期第次" width="90" align="center" />
|
||||
<el-table-column prop="xs" label="学时" width="70" align="center" />
|
||||
<el-table-column prop="klx" label="课类型" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="完成情况" width="90" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag :type="row.completed ? 'success' : 'info'" size="mini">{{ row.completed ? '已完成' : '未完成' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div slot="footer">
|
||||
<el-button size="small" @click="planVisible = false">取消</el-button>
|
||||
<el-button size="small" type="primary" :disabled="!planCurrent || planCurrent.completed" @click="confirmPlan">加入当前学期</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ==================== 目标班次学期选取弹窗 ==================== -->
|
||||
<ClassTeamSelectDialog
|
||||
:visible="teamSelectVisible"
|
||||
@@ -325,10 +384,17 @@ import {
|
||||
batchCopyCalendarBySemester,
|
||||
addTeamTask,
|
||||
delTeamTask,
|
||||
batchPresetMerge,
|
||||
batchPresetSplit,
|
||||
autoGenerateRequiredCourses
|
||||
autoGenerateRequiredCourses,
|
||||
listPlanCourses,
|
||||
addTaskFromLibrary,
|
||||
addTaskFromPlan,
|
||||
syncTaskFromPlan,
|
||||
batchDeleteTasks,
|
||||
pasteTasks
|
||||
} from '@/api/studentRecords/classCalendar'
|
||||
import { listKb } from '@/api/teachOffice/kb'
|
||||
|
||||
const TASK_CLIPBOARD_KEY = 'education.classTaskClipboard'
|
||||
|
||||
export default {
|
||||
name: 'ClassCalendarDialog',
|
||||
@@ -358,7 +424,17 @@ export default {
|
||||
// ==================== 复制到其它班次 ====================
|
||||
teamSelectVisible: false,
|
||||
applyMode: 'region',
|
||||
copyNote: ''
|
||||
copyNote: '',
|
||||
clipboardCount: 0,
|
||||
planMap: {},
|
||||
libraryVisible: false,
|
||||
libraryKeyword: '',
|
||||
libraryRows: [],
|
||||
libraryCurrent: null,
|
||||
planVisible: false,
|
||||
planRows: [],
|
||||
planCurrent: null,
|
||||
pickerLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -398,12 +474,13 @@ export default {
|
||||
return { totalXs, totalXf }
|
||||
},
|
||||
editTitle() {
|
||||
return this.editMode === 'edit' ? '编辑班历记录' : '新增班历记录'
|
||||
return this.editMode === 'edit' ? '编辑课程任务' : '新增课程任务'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) {
|
||||
this.clipboardCount = this.readClipboard().length
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
@@ -411,8 +488,21 @@ export default {
|
||||
methods: {
|
||||
/* ---------- 格式化 ---------- */
|
||||
fmtDate(val) {
|
||||
if (!val) return '-'
|
||||
return String(val).slice(0, 10)
|
||||
if (val == null || val === '') return '-'
|
||||
if (Array.isArray(val) && val.length >= 3) {
|
||||
return `${val[0]}-${String(val[1]).padStart(2, '0')}-${String(val[2]).padStart(2, '0')}`
|
||||
}
|
||||
if (typeof val === 'object') {
|
||||
const year = val.year
|
||||
const month = val.monthValue || val.month
|
||||
const day = val.dayOfMonth || val.day
|
||||
if (year && month && day) {
|
||||
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
const text = String(val)
|
||||
return text.length >= 10 ? text.slice(0, 10) : text
|
||||
},
|
||||
xqdcLabel(val) {
|
||||
if (val === 1 || val === '1') return '第1学期'
|
||||
@@ -436,14 +526,44 @@ export default {
|
||||
const s = this.semester || {}
|
||||
if (!s.bh) return
|
||||
this.loading = true
|
||||
listClassCalendar(s.bh).then(res => {
|
||||
this.records = (res && res.data) || []
|
||||
Promise.all([
|
||||
listClassCalendar(s.bh),
|
||||
listPlanCourses(s.bh, false).catch(() => ({ data: [] }))
|
||||
]).then(([taskRes, planRes]) => {
|
||||
this.records = (taskRes && taskRes.data) || []
|
||||
const plans = (planRes && planRes.data) || []
|
||||
const map = {}
|
||||
plans.forEach(item => {
|
||||
if (item.kbh) map[item.kbh] = item
|
||||
})
|
||||
this.planMap = map
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.records = []
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
isPlanMismatch(row) {
|
||||
const plan = row && row.kbh ? this.planMap[row.kbh] : null
|
||||
if (!plan) return false
|
||||
return String(row.xs || '') !== String(plan.xs || '')
|
||||
|| String(row.klx || '') !== String(plan.klx || '')
|
||||
|| String(row.xf || '') !== String(plan.xf || '')
|
||||
|| String(row.jc || '') !== String(plan.jc || '')
|
||||
|| String(row.zks || '') !== String(plan.zks || '')
|
||||
},
|
||||
rowClassName({ row }) {
|
||||
return this.isPlanMismatch(row) ? 'plan-mismatch-row' : ''
|
||||
},
|
||||
readClipboard() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(TASK_CLIPBOARD_KEY)
|
||||
const parsed = raw ? JSON.parse(raw) : []
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
handleSelectionChange(rows) {
|
||||
this.selection = rows
|
||||
@@ -529,7 +649,7 @@ export default {
|
||||
req.then(() => {
|
||||
this.saving = false
|
||||
this.editVisible = false
|
||||
this.$message.success(this.editMode === 'edit' ? '班历记录已更新' : '班历记录已新增')
|
||||
this.$message.success(this.editMode === 'edit' ? '课程任务已更新' : '课程任务已新增')
|
||||
this.loadData()
|
||||
}).catch(() => {
|
||||
this.saving = false
|
||||
@@ -539,7 +659,7 @@ export default {
|
||||
|
||||
/* ---------- 删除 ---------- */
|
||||
handleDelete(row) {
|
||||
this.$confirm(`确定删除该班历记录(${row.jc || row.bh})吗?`, '提示', {
|
||||
this.$confirm(`确定删除该课程任务(${row.jc || row.bh})吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
@@ -571,65 +691,36 @@ export default {
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
/* ---------- 预设合班 / 拆班 ---------- */
|
||||
handlePresetMerge() {
|
||||
const bhs = this.selection.map(r => r.bh)
|
||||
if (!bhs.length) {
|
||||
this.$message.warning('请先勾选要合班的班历记录')
|
||||
return
|
||||
}
|
||||
this.$confirm(`将按年度为选中的 ${bhs.length} 条记录设置统一的课次序号,是否继续?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
batchPresetMerge(bhs).then(res => {
|
||||
const count = (res && res.data) || 0
|
||||
this.$message.success(`已完成 ${count} 条记录的预设合班`)
|
||||
this.loadData()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
handlePresetSplit() {
|
||||
const bhs = this.selection.map(r => r.bh)
|
||||
if (!bhs.length) {
|
||||
this.$message.warning('请先勾选要拆班的班历记录')
|
||||
return
|
||||
}
|
||||
this.$confirm(`将清空选中的 ${bhs.length} 条记录的课次序号,是否继续?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
batchPresetSplit(bhs).then(res => {
|
||||
const count = (res && res.data) || 0
|
||||
this.$message.success(`已完成 ${count} 条记录的预设拆班`)
|
||||
this.loadData()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
/* ---------- 应用于其它班次 ---------- */
|
||||
handleApplyRegion() {
|
||||
const bhs = this.selection.map(r => r.bh)
|
||||
if (!bhs.length) {
|
||||
this.$message.warning('请先勾选要应用的班历记录(选定区域)')
|
||||
this.$message.warning('请先勾选要应用的课程任务(选定区域)')
|
||||
return
|
||||
}
|
||||
this.applyMode = 'region'
|
||||
this.copyNote = `将选中的 ${bhs.length} 条班历记录复制到所选班次`
|
||||
this.copyNote = `将选中的 ${bhs.length} 条课程任务复制到所选班次`
|
||||
this.teamSelectVisible = true
|
||||
},
|
||||
|
||||
handleApplyWhole() {
|
||||
this.applyMode = 'whole'
|
||||
this.copyNote = '将整个班历复制到所选班次'
|
||||
this.copyNote = '将整个班次教学任务复制到所选班次'
|
||||
this.teamSelectVisible = true
|
||||
},
|
||||
|
||||
handleTeamConfirm(targets) {
|
||||
if (!targets || !targets.length) return
|
||||
if (this.applyMode === 'paste') {
|
||||
const bhList = this.readClipboard()
|
||||
Promise.all(targets.map(item => pasteTasks(bhList, item.bh))).then(results => {
|
||||
const count = results.reduce((sum, res) => sum + ((res && res.data) || 0), 0)
|
||||
this.$message.success(`已粘贴 ${count} 条独立课程任务`)
|
||||
this.teamSelectVisible = false
|
||||
this.loadData()
|
||||
})
|
||||
return
|
||||
}
|
||||
const targetBhList = targets.map(t => t.bh)
|
||||
if (this.applyMode === 'whole') {
|
||||
batchCopyCalendarBySemester({
|
||||
@@ -637,7 +728,7 @@ export default {
|
||||
targetBhList: targetBhList
|
||||
}).then(res => {
|
||||
const data = (res && res.data) || {}
|
||||
this.$message.success(`已复制整个班历,共新增 ${data.count || 0} 条记录`)
|
||||
this.$message.success(`已复制整个班次教学任务,共新增 ${data.count || 0} 条记录`)
|
||||
this.loadData()
|
||||
})
|
||||
} else {
|
||||
@@ -652,10 +743,108 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
handleCopySelected() {
|
||||
const bhList = this.selection.map(item => item.bh).filter(Boolean)
|
||||
if (!bhList.length) return
|
||||
sessionStorage.setItem(TASK_CLIPBOARD_KEY, JSON.stringify(bhList))
|
||||
this.clipboardCount = bhList.length
|
||||
this.$message.success(`已复制 ${bhList.length} 条课程任务,可在其它班次粘贴`)
|
||||
},
|
||||
handlePaste() {
|
||||
if (!this.readClipboard().length) {
|
||||
this.$message.warning('剪贴板没有课程任务')
|
||||
return
|
||||
}
|
||||
this.applyMode = 'paste'
|
||||
this.copyNote = `将剪贴板中的 ${this.clipboardCount} 条课程任务粘贴为独立记录`
|
||||
this.teamSelectVisible = true
|
||||
},
|
||||
handleSyncPlan() {
|
||||
const bhList = this.selection.map(item => item.bh).filter(Boolean)
|
||||
if (!bhList.length) return
|
||||
this.$confirm('将按人培覆盖所选行的学时、类型、学分和周课时,是否继续?', '同步人培', { type: 'warning' }).then(() => {
|
||||
syncTaskFromPlan(bhList).then(res => {
|
||||
this.$message.success(`已同步 ${res.data || 0} 条`)
|
||||
this.loadData()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleBatchDelete() {
|
||||
const bhList = this.selection.map(item => item.bh).filter(Boolean)
|
||||
if (!bhList.length) return
|
||||
this.$confirm('已发布到任务书或已排课的行会拒绝删除,其余将物理删除。', '批量删除', { type: 'warning' }).then(() => {
|
||||
batchDeleteTasks(bhList).then(res => {
|
||||
this.$message.success(`已删除 ${res.data || 0} 条`)
|
||||
this.loadData()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
openLibraryPicker() {
|
||||
this.libraryVisible = true
|
||||
this.libraryCurrent = null
|
||||
this.loadLibrary()
|
||||
},
|
||||
loadLibrary() {
|
||||
this.pickerLoading = true
|
||||
const keyword = (this.libraryKeyword || '').trim()
|
||||
const unwrap = res => {
|
||||
const data = (res && res.data) || {}
|
||||
return data.records || (Array.isArray(data) ? data : [])
|
||||
}
|
||||
const requests = keyword
|
||||
? [listKb({ pageNum: 1, pageSize: 50, kmc: keyword }), listKb({ pageNum: 1, pageSize: 50, jc: keyword })]
|
||||
: [listKb({ pageNum: 1, pageSize: 50 })]
|
||||
Promise.all(requests).then(results => {
|
||||
const merged = []
|
||||
const seen = {}
|
||||
results.forEach(res => {
|
||||
unwrap(res).forEach(item => {
|
||||
if (!item.kbh || seen[item.kbh]) return
|
||||
seen[item.kbh] = true
|
||||
merged.push(item)
|
||||
})
|
||||
})
|
||||
this.libraryRows = merged
|
||||
this.pickerLoading = false
|
||||
}).catch(() => {
|
||||
this.libraryRows = []
|
||||
this.pickerLoading = false
|
||||
})
|
||||
},
|
||||
confirmLibrary() {
|
||||
if (!this.libraryCurrent || !this.semester) return
|
||||
addTaskFromLibrary(this.semester.bh, this.libraryCurrent.kbh).then(() => {
|
||||
this.libraryVisible = false
|
||||
this.$message.success('已从课程库加入,学时和类型已带出')
|
||||
this.loadData()
|
||||
})
|
||||
},
|
||||
openPlanPicker() {
|
||||
if (!this.semester || !this.semester.bh) return
|
||||
this.planVisible = true
|
||||
this.planCurrent = null
|
||||
this.pickerLoading = true
|
||||
listPlanCourses(this.semester.bh, true).then(res => {
|
||||
this.planRows = res.data || []
|
||||
this.pickerLoading = false
|
||||
}).catch(() => {
|
||||
this.planRows = []
|
||||
this.pickerLoading = false
|
||||
})
|
||||
},
|
||||
confirmPlan() {
|
||||
if (!this.planCurrent || !this.semester) return
|
||||
addTaskFromPlan(this.semester.bh, this.planCurrent.bh).then(() => {
|
||||
this.planVisible = false
|
||||
this.$message.success('已加入当前学期任务')
|
||||
this.loadData()
|
||||
})
|
||||
},
|
||||
|
||||
/* ---------- 刷新 ---------- */
|
||||
handleRefresh() {
|
||||
this.loadData()
|
||||
this.$message.success('已刷新班历')
|
||||
this.$message.success('已刷新班次教学任务')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,6 +924,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .plan-mismatch-row > td {
|
||||
background: #fde2e2 !important;
|
||||
}
|
||||
|
||||
// ========== 弹窗内表单分区标题 ==========
|
||||
.edit-form {
|
||||
.form-section-title {
|
||||
|
||||
Reference in New Issue
Block a user