通过AI比对原操作文档,生成系统功能完善方案,根据方案进行增补,完善从学期新增到最终的排课功能
This commit is contained in:
@@ -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 === '已结束'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user