通过AI比对原操作文档,生成系统功能完善方案,根据方案进行增补,完善从学期新增到最终的排课功能

This commit is contained in:
2026-09-15 14:42:43 +08:00
parent 5187435285
commit e8a58b19fe
54 changed files with 4577 additions and 134 deletions
@@ -95,12 +95,14 @@ export function batchPresetSplit(bhList) {
})
}
// 自动生成必修课程(按学员队+学期第次从专业教学计划生成班历记录
export function autoGenerateRequiredCourses(xydbh, xqdc) {
// 自动生成必修课程(按学员队学期编号 xydxqbh 定位唯一的目标班次学期
// 不再传 (学员队编号, 学期第次):同一学员队在多个年度会有学期第次相同的班次学期,
// 只传这两个参数无法区分学年,会把课程任务写进别的年度。
export function autoGenerateRequiredCourses(xydxqbh) {
return request({
url: '/studentTeamTask/autoGenerateRequiredCourses',
method: 'post',
params: { xydbh: xydbh, xqdc: xqdc }
params: { xydxqbh: xydxqbh }
})
}
@@ -0,0 +1,59 @@
import request from '@/utils/request'
// 教学配当(阶段 3
// 配当只做排课粗约束(周学时分布、同开同结),不直接写实施课程表。
// 配当编辑器视图:周轴(每周可排正课)+ 任务行(含铺学时建议)
export function allocationView(xydxqbh) {
return request({
url: '/teachingAllocation/view',
method: 'get',
params: { xydxqbh }
})
}
// 保存配当:单条(列表 1 个元素)或多选批量;元素 saveGroup=true 时整编组同开同结
export function allocationSave(items) {
return request({
url: '/teachingAllocation/save',
method: 'post',
data: items
})
}
// 优选序数上移/下移(direction: up/down),返回归一化后的任务编号顺序
export function allocationMove(xydxqbh, bh, direction) {
return request({
url: '/teachingAllocation/move',
method: 'post',
params: { xydxqbh, bh, direction }
})
}
// 编组管理面板:同科目全部开课班次
export function allocationGroups(kbh) {
return request({
url: '/teachingAllocation/groups',
method: 'get',
params: { kbh }
})
}
// 编组管理:把所选任务统一改编组号
export function allocationSetGroup(data) {
return request({
url: '/teachingAllocation/setGroup',
method: 'post',
data
})
}
// 导出配当 Excel(xydxqbh 逗号分隔,支持批量)
export function allocationExport(xydxqbhList) {
return request({
url: '/teachingAllocation/export',
method: 'get',
params: { xydxqbh: xydxqbhList.join(',') },
responseType: 'blob'
})
}
@@ -0,0 +1,69 @@
import request from '@/utils/request'
/**
* 教研室任务书填报(阶段 4
* 接口依据:/taskBookFill/*(前端 baseURL 为 /api,此处不重复 /api 前缀)
*/
/** 填报列表 GET /taskBookFill/list?jxrwbh= */
export function taskBookList(jxrwbh) {
return request({
url: '/taskBookFill/list',
method: 'get',
params: { jxrwbh }
})
}
/** 手动合班 POST /taskBookFill/merge body: { bhList } */
export function taskBookMerge(bhList) {
return request({
url: '/taskBookFill/merge',
method: 'post',
data: { bhList }
})
}
/** 按预设合班 POST /taskBookFill/mergeByPreset body: { bhList } */
export function taskBookMergeByPreset(bhList) {
return request({
url: '/taskBookFill/mergeByPreset',
method: 'post',
data: { bhList }
})
}
/** 拆班 POST /taskBookFill/split body: { bhList } */
export function taskBookSplit(bhList) {
return request({
url: '/taskBookFill/split',
method: 'post',
data: { bhList }
})
}
/** 责任教员指定 POST /taskBookFill/setTeacher body: { bh, mode, jybh } */
export function taskBookSetTeacher(data) {
return request({
url: '/taskBookFill/setTeacher',
method: 'post',
data
})
}
/** 场地指定 POST /taskBookFill/setRoom body: { bh, jsbh, useSpecial } */
export function taskBookSetRoom(data) {
return request({
url: '/taskBookFill/setRoom',
method: 'post',
data
})
}
/** 填报字段 POST /taskBookFill/fill body: { bh, jysjhjybh, jsbh, jysjhbz } */
export function taskBookFill(data) {
return request({
url: '/taskBookFill/fill',
method: 'post',
data
})
}
@@ -442,6 +442,10 @@ export default {
cellClass(wIdx, col) {
const classes = []
if (this.selectedKeys.includes(this.cellKey(wIdx, col.colIndex))) classes.push('is-selected')
// 班历:班次日期范围外的格子不可排,给出明确视觉提示
if (this.mode === 'class' && this.isOutOfRange(wIdx, col.dayIndex)) {
classes.push('is-out-of-range')
}
const ev = this.getEvent(wIdx, col.colIndex)
// 可排课的全部白色;不可排课且事件名非空才标红
if (ev && ev.name && !ev.schedulable) {
@@ -449,6 +453,30 @@ export default {
}
return classes
},
// 时间格对应的实际日期
cellDate(wIdx, dayIndex) {
if (!this.calendarStart) return null
const d = new Date(this.calendarStart)
d.setDate(this.calendarStart.getDate() + wIdx * 7 + dayIndex)
return d
},
// 班历只有 [开学日期, 结束日期] 内的格子会落库,范围外的格子无法保存
isOutOfRange(wIdx, dayIndex) {
if (!this.startDate || !this.endDate) return false
const d = this.cellDate(wIdx, dayIndex)
if (!d) return false
const at = v => new Date(v.getFullYear(), v.getMonth(), v.getDate()).getTime()
return at(d) < at(this.startDate) || at(d) > at(this.endDate)
},
// 返回所选时间格里落在班次日期范围外的格子数
countOutOfRangeSelected() {
let n = 0
this.selectedKeys.forEach(key => {
const pos = this.parseCellKey(key)
if (pos && this.isOutOfRange(pos.wIdx, pos.dayIndex)) n++
})
return n
},
/* ---------- 点击 / 拖拽多选 ---------- */
onGridMouseDown(e) {
const cell = e.target.closest('.sce-cell')
@@ -548,6 +576,14 @@ export default {
this.$message.warning('请输入事件名')
return
}
// 班历只在班次日期范围内落库,范围外的格子直接提示,避免落成含糊的“保存失败”
if (this.mode === 'class') {
const outside = this.countOutOfRangeSelected()
if (outside > 0) {
this.$message.warning(`所选 ${outside} 个时间格在班次日期范围外,不可排课`)
return
}
}
const keys = [...this.selectedKeys]
keys.forEach(key => {
const prev = this.events[key]
@@ -876,6 +912,14 @@ export default {
// 可排课白色,不可排课且有事件标红
&.no-schedule { background: #fde2e2; }
// 班历:班次日期范围外的格子不可排
&.is-out-of-range {
background: #f4f4f5;
cursor: not-allowed;
.sce-date-num { color: #dcdfe6; }
}
&.is-selected {
outline: 2px solid var(--edu-green-primary, #00875a);
outline-offset: -2px;
@@ -116,7 +116,7 @@
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="课类型">
<el-form-item label="课类型" prop="klx">
<el-input v-model="editForm.klx" placeholder="如 必修/选修/实践" clearable />
</el-form-item>
</el-col>
@@ -165,7 +165,7 @@
<div class="form-section-title">学时学分</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="学时">
<el-form-item label="学时" prop="xs">
<el-input-number v-model="editForm.xs" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
@@ -233,7 +233,7 @@
<div class="form-section-title">教研室计划</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="教研室代号">
<el-form-item label="教研室代号" prop="jysdh">
<el-input v-model="editForm.jysdh" placeholder="教研室代号" clearable />
</el-form-item>
</el-col>
@@ -418,7 +418,11 @@ export default {
editMode: 'add',
editForm: this.createEmptyEditForm(),
editRules: {
jc: [{ required: true, message: '请输入课程简称', trigger: 'blur' }]
jc: [{ required: true, message: '请输入课程简称', trigger: 'blur' }],
// 以下三列在库中「非空且无默认值」,留空会写入无意义值,这里前置拦截
xs: [{ required: true, message: '请输入学时', trigger: 'blur' }],
klx: [{ required: true, message: '请输入课类型', trigger: 'blur' }],
jysdh: [{ required: true, message: '请输入教研室代号', trigger: 'blur' }]
},
// ==================== 复制到其它班次 ====================
@@ -427,6 +431,7 @@ export default {
copyNote: '',
clipboardCount: 0,
planMap: {},
planByKbh: {},
libraryVisible: false,
libraryKeyword: '',
libraryRows: [],
@@ -532,19 +537,31 @@ export default {
]).then(([taskRes, planRes]) => {
this.records = (taskRes && taskRes.data) || []
const plans = (planRes && planRes.data) || []
const map = {}
// 同一课编号可能出现在人培的多个学期第次里,按「课编号@学期第次」精确配对,
// 再退化为只按课编号配对,避免拿别的学期的人培行来判「人培差异」。
const exact = {}
const byKbh = {}
plans.forEach(item => {
if (item.kbh) map[item.kbh] = item
if (!item.kbh) return
if (item.xqdc !== undefined && item.xqdc !== null && item.xqdc !== '') {
exact[item.kbh + '@' + item.xqdc] = item
}
if (!byKbh[item.kbh]) byKbh[item.kbh] = item
})
this.planMap = map
this.planMap = exact
this.planByKbh = byKbh
this.loading = false
}).catch(() => {
this.records = []
this.loading = false
})
},
findPlan(row) {
if (!row || !row.kbh) return null
return this.planMap[row.kbh + '@' + row.xqdc] || this.planByKbh[row.kbh] || null
},
isPlanMismatch(row) {
const plan = row && row.kbh ? this.planMap[row.kbh] : null
const plan = this.findPlan(row)
if (!plan) return false
return String(row.xs || '') !== String(plan.xs || '')
|| String(row.klx || '') !== String(plan.klx || '')
@@ -674,7 +691,7 @@ export default {
/* ---------- 自动生成必修课程 ---------- */
handleAutoGenerate() {
const s = this.semester || {}
if (!s.xydbh || !s.xqdc) {
if (!s.bh || !s.xydbh || !s.xqdc) {
this.$message.warning('当前班次学期缺少学员队编号或学期第次,无法自动生成')
return
}
@@ -683,7 +700,9 @@ export default {
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
autoGenerateRequiredCourses(s.xydbh, s.xqdc).then(res => {
// 传班次学期编号:同一学员队在多个年度可能有学期第次相同的班次学期,
// 只用 (学员队编号, 学期第次) 会写进别的学年。
autoGenerateRequiredCourses(s.bh).then(res => {
const count = (res && res.data) || 0
this.$message.success(`已自动生成 ${count} 门必修课程`)
this.loadData()
@@ -0,0 +1,452 @@
<template>
<el-dialog
:visible="visible"
:title="`教学配当 - ${semesterName}`"
width="96%"
top="4vh"
:close-on-click-modal="false"
@update:visible="val => $emit('update:visible', val)"
>
<div v-loading="loading" class="allocation-page">
<!-- ==================== 1. 学期概览 ==================== -->
<el-card shadow="never" class="block-card">
<div class="meta-line">
<span>学期{{ view.kxrq }} ~ {{ view.jsrq }}</span>
<span class="ml"> {{ view.totalWeeks }} </span>
<span class="ml">
学期代号
<el-tag size="mini" type="info">{{ view.ndCode }}</el-tag>
</span>
<span v-if="view.frozen" class="ml frozen-tip">
<el-tag size="mini" type="danger">教学任务已发布/已结束配当只读</el-tag>
</span>
</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>
</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>
</tr>
</thead>
</table>
</div>
</el-card>
<!-- ==================== 2. 工具栏 ==================== -->
<div class="list-header">
<div class="list-title">课程配当铺学时为排课粗约束建议不写实施课表</div>
<div class="list-actions">
<el-button size="small" type="primary" :disabled="!selection.length || view.frozen"
@click="handleSaveSelected">保存所选</el-button>
<el-button size="small" icon="el-icon-refresh" @click="loadData">刷新</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>
</div>
<!-- ==================== 3. 配当表格=任务= ==================== -->
<div class="alloc-table-wrap" ref="allocTableWrap">
<table class="alloc-table">
<thead>
<tr>
<th class="sticky-col c-check">
<el-checkbox :disabled="view.frozen" @change="val => handleCheckAll(val)" />
</th>
<th class="sticky-col c-idx"></th>
<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-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>
</tr>
</thead>
<tbody>
<tr v-for="row in view.tasks" :key="row.bh" :class="{ 'row-overflow': row.overflowHours > 0 }">
<td class="sticky-col c-check">
<el-checkbox v-model="row._checked" :disabled="view.frozen" @change="syncSelection" />
</td>
<td class="sticky-col c-idx">{{ row.pdxh || '-' }}</td>
<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">
<el-input-number v-model="row.pdxh" :min="1" :controls="false" size="mini"
class="tiny-input" :disabled="view.frozen" />
</td>
<td class="sticky-col c-num">
<el-input-number v-model="row.pdqsz" :min="1" :max="view.totalWeeks" :controls="false"
size="mini" class="tiny-input" :disabled="view.frozen" />
</td>
<td class="sticky-col c-mode">
<el-select v-model="row.pdaz" size="mini" class="mode-select" :disabled="view.frozen">
<el-option label="按周" :value="1" />
<el-option label="连排" :value="0" />
</el-select>
</td>
<td class="sticky-col c-num">
<el-input-number v-model="row.pdbz" :min="0" :controls="false" size="mini"
class="tiny-input" :disabled="view.frozen" />
</td>
<td class="sticky-col c-ops">
<el-button type="text" size="mini" :disabled="view.frozen" @click="handleSaveRow(row)">保存</el-button>
<el-button type="text" size="mini" :disabled="view.frozen"
@click="handleSaveRow(row, true)">整组</el-button>
<el-button type="text" size="mini" :disabled="view.frozen" @click="handleMove(row, 'up')"></el-button>
<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>
</tr>
<tr v-if="!view.tasks.length">
<td :colspan="10 + view.weeks.length" class="empty-row">
该班次学期暂无课程任务请先在班次教学任务中生成/添加课程
</td>
</tr>
</tbody>
</table>
</div>
<!-- ==================== 4. 编组管理面板 ==================== -->
<el-dialog
:visible.sync="groupPanelVisible"
:title="`编组管理 - ${groupCourse}`"
width="720px"
append-to-body
:close-on-click-modal="false"
>
<div class="group-panel">
<div class="group-tip">同科目全部开课班次勾选后统一改编组号同组同开同结</div>
<el-table ref="groupTableRef" :data="groupList" border size="mini" max-height="320"
@selection-change="val => groupSelection = val">
<el-table-column type="selection" width="45" align="center" />
<el-table-column prop="xydmc" label="班次" min-width="160" show-overflow-tooltip />
<el-table-column prop="nd" label="学期代号" width="90" align="center" />
<el-table-column prop="xs" label="学时" width="60" align="center" />
<el-table-column prop="zks" label="周课时" width="70" align="center" />
<el-table-column prop="pdqsz" label="起始周" width="70" align="center" />
<el-table-column label="连排/按周" width="90" align="center">
<template slot-scope="{ row }">{{ row.pdaz === 0 ? '连排' : '按周' }}</template>
</el-table-column>
<el-table-column prop="pdbz" label="当前编组" width="80" align="center" />
</el-table>
<div class="group-ops">
<span>目标编组号:</span>
<el-input-number v-model="groupTarget" :min="0" :controls="false" size="mini" class="tiny-input" />
<el-button size="mini" type="primary" :disabled="!groupSelection.length" @click="applyGroup">应用</el-button>
</div>
</div>
</el-dialog>
</div>
</el-dialog>
</template>
<script>
import {
allocationView,
allocationSave,
allocationMove,
allocationGroups,
allocationSetGroup,
allocationExport
} from '@/api/teachBusiness/allocation'
const SOURCE_TEXT = { class: '班历', school: '校历', default: '默认(30学时)' }
export default {
name: 'TeachingAllocationDialog',
props: {
visible: { type: Boolean, default: false },
/** 选中的班次学期记录(含 bh/xydbh/xqdc/nd/xydmc/kxrq/jsrq */
semester: { type: Object, default: null }
},
data() {
return {
loading: false,
view: { weeks: [], tasks: [], totalWeeks: 0, frozen: false, ndCode: '', kxrq: '', jsrq: '', xydmc: '' },
selection: [],
groupPanelVisible: false,
groupCourse: '',
groupKbh: '',
groupList: [],
groupSelection: [],
groupTarget: 0
}
},
computed: {
semesterName() {
const s = this.semester || {}
return s.xydmc || s.xydbh || ''
}
},
watch: {
visible(val) {
if (val) {
this.loadData()
}
}
},
methods: {
loadData() {
const s = this.semester || {}
if (!s.bh) {
this.$message.warning('缺少班次学期编号')
return
}
this.loading = true
allocationView(s.bh).then(res => {
const view = (res && res.data) || {}
view.tasks = (view.tasks || []).map(t => ({ ...t, _checked: false }))
this.view = view
this.selection = []
}).finally(() => {
this.loading = false
})
},
/* ---------- 选择 ---------- */
handleCheckAll(val) {
this.view.tasks.forEach(t => { t._checked = !!val && !this.view.frozen })
this.syncSelection()
},
syncSelection() {
this.selection = this.view.tasks.filter(t => t._checked)
},
/* ---------- 保存 ---------- */
buildItem(row, saveGroup) {
return {
bh: row.bh,
pdxh: row.pdxh,
pdqsz: row.pdqsz,
pdaz: row.pdaz,
pdbz: row.pdbz,
saveGroup: !!saveGroup
}
},
handleSaveRow(row, saveGroup) {
if (saveGroup) {
this.$confirm(`将按「同科目 + 编组 ${row.pdbz || 0}」把本学期所有同组任务一起修改(同开同结),是否继续?`, '保存整个编组', {
type: 'warning'
}).then(() => {
this.doSave([this.buildItem(row, true)], `已保存整编组`)
}).catch(() => {})
return
}
this.doSave([this.buildItem(row, false)], '已保存')
},
handleSaveSelected() {
if (!this.selection.length) {
this.$message.warning('请先勾选要保存的行')
return
}
this.doSave(this.selection.map(row => this.buildItem(row, false)), `已保存 ${this.selection.length} 条`)
},
doSave(items, okText) {
allocationSave(items).then(res => {
this.$message.success(`${okText}(更新 ${res && res.data || 0} 行)`)
this.loadData()
})
},
/* ---------- 上移/下移 ---------- */
handleMove(row, direction) {
const s = this.semester || {}
allocationMove(s.bh, row.bh, direction).then(() => {
this.loadData()
})
},
/* ---------- 编组管理 ---------- */
openGroupPanel(row) {
this.groupKbh = row.kbh
this.groupCourse = `${row.kcmc}${row.kbh}`
this.groupTarget = row.pdbz || 0
this.groupSelection = []
allocationGroups(row.kbh).then(res => {
this.groupList = (res && res.data) || []
this.groupPanelVisible = true
})
},
applyGroup() {
allocationSetGroup({
bhList: this.groupSelection.map(item => item.bh),
pdbz: this.groupTarget
}).then(res => {
this.$message.success(`已更新 ${res && res.data || 0} 行`)
this.groupPanelVisible = false
this.loadData()
})
},
/* ---------- 单元格 ---------- */
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)
return {
'cell-active': hours > 0,
'cell-overflow-week': hours > w.availableHours
}
},
isMonthStart(w) {
return w.startDate.getDate() <= 7
},
shortDate(dateStr) {
return dateStr ? String(dateStr).slice(5) : ''
},
sourceText(source) {
return `来源:${SOURCE_TEXT[source] || source}`
},
/* ---------- 导出 ---------- */
handleExportExcel() {
const s = this.semester || {}
if (!s.bh) return
allocationExport([s.bh]).then(blob => {
this.saveBlob(blob, `教学配当_${this.semesterName}.xlsx`)
})
},
handleExportImage() {
this.$nextTick(() => {
const wrap = this.$refs.allocTableWrap
if (!wrap) return
const table = wrap.querySelector('table')
const width = table.scrollWidth
const height = table.scrollHeight
const canvas = document.createElement('canvas')
const dpr = window.devicePixelRatio || 1
canvas.width = width * dpr
canvas.height = height * dpr
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, width, height)
// 表格自绘(无 html2canvas 依赖):逐行输出文本网格
const rows = this.view.tasks
const weeks = this.view.weeks
const lineH = 24
ctx.font = '12px sans-serif'
let y = 20
ctx.fillText(`${this.semesterName} 教学配当 ${this.view.kxrq}~${this.view.jsrq}(共${weeks.length}周)`, 10, y)
y += lineH
ctx.fillText('周次', 10, y)
weeks.forEach(w => {
ctx.fillText('W' + w.weekNo, 260 + (w.weekNo - 1) * 34, y)
})
y += lineH
ctx.fillText('可排正课', 10, y)
weeks.forEach(w => {
ctx.fillText(w.availableHours + 'h', 260 + (w.weekNo - 1) * 34, y)
})
y += lineH
rows.forEach(row => {
ctx.fillText(String(row.pdxh || '-'), 10, y)
ctx.fillText(String(row.kcmc || '').slice(0, 12), 60, y)
ctx.fillText(`学时${row.xs}/周${row.zks}`, 150, y)
weeks.forEach(w => {
const h = this.cellHours(row, w.weekNo)
if (h > 0) {
ctx.fillStyle = '#409eff'
ctx.fillRect(254 + (w.weekNo - 1) * 34, y - 12, 30, 16)
ctx.fillStyle = '#fff'
ctx.fillText(String(h), 260 + (w.weekNo - 1) * 34, y)
ctx.fillStyle = '#000'
}
})
y += lineH
})
const link = document.createElement('a')
link.download = `教学配当_${this.semesterName}.png`
link.href = canvas.toDataURL('image/png')
link.click()
})
},
saveBlob(blob, fileName) {
const url = window.URL.createObjectURL(new Blob([blob]))
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
window.URL.revokeObjectURL(url)
}
}
}
</script>
<style lang="scss" scoped>
.allocation-page {
.block-card { margin-bottom: 8px; }
.meta-line {
display: flex; align-items: center; flex-wrap: wrap;
font-size: 13px; color: #606266; margin-bottom: 8px;
.ml { margin-left: 16px; }
.frozen-tip { margin-left: auto; }
}
.week-axis { overflow-x: auto; }
.axis-table, .alloc-table {
border-collapse: collapse; font-size: 12px; width: max-content; min-width: 100%;
th, td { border: 1px solid #ebeef5; padding: 4px 6px; text-align: center; }
}
.week-col { min-width: 34px; }
.month-start { border-left: 2px solid #dcdfe6 !important; }
.hours-cell { color: #409eff; font-weight: 600; }
.alloc-table-wrap { overflow: auto; max-height: 52vh; border: 1px solid #ebeef5; }
.alloc-table {
.sticky-col { position: sticky; background: #fafafa; z-index: 2; }
.c-check { left: 0; width: 36px; }
.c-idx { left: 36px; width: 36px; }
.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; }
.cell { min-width: 34px; color: #909399; }
.cell-active { background: #d9ecff; color: #1f5faa; font-weight: 600; }
.cell-overflow-week { background: #fde2e2; }
.row-overflow .c-name { color: #f56c6c; }
.empty-row { padding: 16px; color: #909399; text-align: center; }
}
.list-header {
display: flex; align-items: center; justify-content: space-between; margin: 4px 0 8px;
.list-title { font-size: 13px; font-weight: 600; color: #303133; }
}
.tiny-input { width: 52px; }
.tiny-input ::v-deep input { padding: 0 4px; text-align: center; }
.mode-select { width: 66px; }
.group-panel {
.group-tip { font-size: 12px; color: #909399; margin-bottom: 8px; }
.group-ops { margin-top: 10px; display: flex; align-items: center; gap: 8px; }
}
}
</style>
@@ -63,6 +63,7 @@
<div class="right-group">
<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>
</div>
</div>
@@ -170,6 +171,12 @@
@update:visible="val => eventCalendarVisible = val"
/>
<TeachingAllocationDialog
:visible="allocationVisible"
:semester="allocationSemester"
@update:visible="val => allocationVisible = val"
/>
<!-- ==================== 批量创建班次学期弹窗 ==================== -->
<el-dialog
:visible="batchAddVisible"
@@ -320,6 +327,7 @@
import ClassSemesterDetailDialog from './ClassSemesterDetailDialog.vue'
import ClassCalendarDialog from './ClassCalendarDialog.vue'
import ClassEventCalendarDialog from './ClassEventCalendarDialog.vue'
import TeachingAllocationDialog from './TeachingAllocationDialog.vue'
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'
@@ -330,7 +338,8 @@ export default {
components: {
ClassSemesterDetailDialog,
ClassCalendarDialog,
ClassEventCalendarDialog
ClassEventCalendarDialog,
TeachingAllocationDialog
},
data() {
return {
@@ -628,6 +637,16 @@ export default {
this.eventCalendarVisible = true
},
handleAllocation(row) {
const target = row || this.currentRow
if (!target) {
this.$message.warning('请先在列表中选择一条班次学期信息')
return
}
this.allocationSemester = target
this.allocationVisible = true
},
handleBatchGenerateTasks() {
if (!this.selection.length) {
this.$message.warning('请先勾选班次学期')
@@ -63,10 +63,11 @@
<el-table-column prop="jssj" label="结束时间" width="180" align="center" :formatter="fmtDateTime" />
<el-table-column prop="cjsj" label="创建时间" width="180" align="center" :formatter="fmtDateTime" />
<el-table-column prop="jcxqscsj" label="教材需求生成时间" align="center" :formatter="fmtDateTime" />
<el-table-column label="操作" width="380" align="center" fixed="right">
<el-table-column label="操作" width="450" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="small" @click="handleDetail(row)">详情</el-button>
<el-button type="text" size="small" :disabled="isTaskLocked(row)" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" @click="handleTaskBook(row)">任务书填报</el-button>
<el-button
type="text"
size="small"
@@ -186,6 +187,14 @@
<el-button @click="detailVisible = false">关闭</el-button>
</div>
</el-dialog>
<!-- ==================== 6. 任务书填报对话框阶段 4 ==================== -->
<task-book-fill-dialog
:visible.sync="taskBookVisible"
:jxrwbh="taskBookRow.bh"
:task-name="taskBookRow.rwmc"
:task-status="taskBookRow.zt"
/>
</div>
</template>
@@ -207,9 +216,11 @@ import {
endPublishTeachingTask
} from '@/api/teachBusiness/teachingTask'
import { listAllSemester } from '@/api/teachBusiness/semester'
import TaskBookFillDialog from './TaskBookFillDialog.vue'
export default {
name: 'TaskPlan',
components: { TaskBookFillDialog },
data() {
return {
// ==================== 1. 查询条件 ====================
@@ -252,7 +263,11 @@ export default {
// ==================== 4. 详情 ====================
detailVisible: false,
detailLoading: false,
detailData: {}
detailData: {},
// ==================== 5. 任务书填报(阶段 4 ====================
taskBookVisible: false,
taskBookRow: {}
}
},
created() {
@@ -441,6 +456,11 @@ export default {
isTaskPublished(row) {
return row && (row.zt === '发布' || row.zt === '已发布')
},
/* ---------- 任务书填报(阶段 4 ---------- */
handleTaskBook(row) {
this.taskBookRow = row || {}
this.taskBookVisible = true
},
isTaskEnded(row) {
return row && row.zt === '已结束'
},