Files
education/frontend/src/views/teachOffice/shiftSemester/ClassCalendarDialog.vue
T

965 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-dialog
:visible="dialogVisible"
:title="`班次教学任务——${semesterTitle}`"
width="1200px"
top="6vh"
:close-on-click-modal="false"
class="class-calendar-dialog"
@update:visible="val => dialogVisible = val"
>
<div v-loading="loading" class="class-calendar">
<!-- ==================== 1. 学期与班次信息 ==================== -->
<div class="info-panel">
<div class="info-title-row">
<span class="semester-name">{{ semesterTitle }}</span>
<span class="class-name">{{ className }}</span>
</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>
</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 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" :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" icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
</div>
</div>
<!-- ==================== 3. 班历记录表格 ==================== -->
<div class="table-area">
<el-table
ref="tableRef"
:data="records"
border
stripe
size="small"
max-height="480"
class="calendar-table"
:row-class-name="rowClassName"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="kcxh" label="课次序号" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="jhkcxh" label="计划课次" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="jc" label="简称" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="kbh" label="课编号" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="klx" label="课类型" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="jybh" label="教员编号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="jsbh" label="教室编号" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="zks" label="周课时" width="70" align="center" show-overflow-tooltip />
<el-table-column prop="xs" label="学时" width="60" align="center" show-overflow-tooltip />
<el-table-column prop="llxs" label="理论学时" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="sjxs" label="实践学时" width="80" align="center" show-overflow-tooltip />
<el-table-column prop="xf" label="学分" width="60" align="center" show-overflow-tooltip />
<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 }">
<el-button type="text" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" size="small" class="text-danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="select-tip">
提示粉红行为与人培不一致可用同步人培按人培覆盖已发布到任务书或已排课的行不能删除复制后粘贴会生成独立任务
</div>
</div>
</div>
<div slot="footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</div>
<!-- ==================== 班历记录新增/编辑弹窗 ==================== -->
<el-dialog
:visible="editVisible"
:title="editTitle"
width="760px"
:close-on-click-modal="false"
append-to-body
@update:visible="val => editVisible = val"
>
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="120px" size="small" class="edit-form">
<div class="form-section-title">基本信息</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="简称" prop="jc">
<el-input v-model="editForm.jc" placeholder="课程简称" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课编号">
<el-input v-model="editForm.kbh" placeholder="课程编号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="课类型" prop="klx">
<el-input v-model="editForm.klx" placeholder="如 必修/选修/实践" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课次序号">
<el-input-number v-model="editForm.kcxh" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="计划课次序号">
<el-input-number v-model="editForm.jhkcxh" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序号标识">
<el-input-number v-model="editForm.xhbs" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="教员编号">
<el-input v-model="editForm.jybh" placeholder="教员编号" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="教室编号">
<el-input v-model="editForm.jsbh" placeholder="教室编号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="人数">
<el-input-number v-model="editForm.rs" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="周课时">
<el-input-number v-model="editForm.zks" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<div class="form-section-title">学时学分</div>
<el-row :gutter="16">
<el-col :span="12">
<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>
<el-col :span="12">
<el-form-item label="理论学时">
<el-input-number v-model="editForm.llxs" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="实践学时">
<el-input-number v-model="editForm.sjxs" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学分">
<el-input-number v-model="editForm.xf" :min="0" :precision="1" :step="0.5" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<div class="form-section-title">考试信息</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="考试课时">
<el-input-number v-model="editForm.ksks" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="考试编号">
<el-input v-model="editForm.ksbh" placeholder="考试编号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="考试地点">
<el-input v-model="editForm.ksdd" placeholder="考试地点" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成绩分制">
<el-input v-model="editForm.cjfz" placeholder="如 百分制/等级制" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="不计入平均分">
<el-select v-model="editForm.bjrxypjf" clearable style="width: 100%">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="考试课时不显示">
<el-select v-model="editForm.ksksbxs" clearable style="width: 100%">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<div class="form-section-title">教研室计划</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="教研室代号" prop="jysdh">
<el-input v-model="editForm.jysdh" placeholder="教研室代号" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划教员编号">
<el-input v-model="editForm.jysjhjybh" placeholder="教研室计划教员编号" clearable />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="计划备注">
<el-input v-model="editForm.jysjhbz" type="textarea" :rows="2" placeholder="教研室计划备注" clearable />
</el-form-item>
<div class="form-section-title">配档设置</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="配档序号">
<el-input-number v-model="editForm.pdxh" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="配档起始周">
<el-input-number v-model="editForm.pdqsz" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="配档按周">
<el-input-number v-model="editForm.pdaz" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="占正课时间">
<el-input-number v-model="editForm.pdzzksj" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="配档编组">
<el-input-number v-model="editForm.pdbz" :min="1" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="同班异课选修编组">
<el-select v-model="editForm.pdtbykxxbz" clearable style="width: 100%">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="启用自定义学时">
<el-select v-model="editForm.pdqyzdyxs" clearable style="width: 100%">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="编组">
<el-input-number v-model="editForm.bz2" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="自定义学时分布">
<el-input v-model="editForm.pdzdyxsfbsj" type="textarea" :rows="2" placeholder="配档自定义学时分布数据" clearable />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="editForm.bz" type="textarea" :rows="2" placeholder="备注" clearable />
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="editVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="saving" @click="handleEditSubmit">确定</el-button>
</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"
:semester-list="semesterOptions"
:exclude-bh="safeSemester.bh"
:exclude-note="copyNote"
@update:visible="val => teamSelectVisible = val"
@confirm="handleTeamConfirm"
/>
</el-dialog>
</template>
<script>
import ClassTeamSelectDialog from './ClassTeamSelectDialog.vue'
import {
listClassCalendar,
updateClassCalendar,
batchCopyCalendar,
batchCopyCalendarBySemester,
addTeamTask,
delTeamTask,
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',
components: { ClassTeamSelectDialog },
props: {
visible: { type: Boolean, default: false },
/** 选中的班次学期记录(含 bh/xydbh/xqdc/nd/xydmc/kxrq/jsrq */
semester: { type: Object, default: null },
/** 可选的目标班次学期列表(已含班次名称的完整列表) */
semesterOptions: { type: Array, default: () => [] }
},
data() {
return {
loading: false,
saving: false,
records: [],
selection: [],
// ==================== 新增/编辑 ====================
editVisible: false,
editMode: 'add',
editForm: this.createEmptyEditForm(),
editRules: {
jc: [{ required: true, message: '请输入课程简称', trigger: 'blur' }],
// 以下三列在库中「非空且无默认值」,留空会写入无意义值,这里前置拦截
xs: [{ required: true, message: '请输入学时', trigger: 'blur' }],
klx: [{ required: true, message: '请输入课类型', trigger: 'blur' }],
jysdh: [{ required: true, message: '请输入教研室代号', trigger: 'blur' }]
},
// ==================== 复制到其它班次 ====================
teamSelectVisible: false,
applyMode: 'region',
copyNote: '',
clipboardCount: 0,
planMap: {},
planByKbh: {},
libraryVisible: false,
libraryKeyword: '',
libraryRows: [],
libraryCurrent: null,
planVisible: false,
planRows: [],
planCurrent: null,
pickerLoading: false
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
},
safeSemester() {
return this.semester || {}
},
className() {
const s = this.safeSemester
return s.xydmc || s.xydbh || ''
},
semesterTitle() {
const s = this.safeSemester
const nd = s.nd !== undefined && s.nd !== null && s.nd !== '' ? `${s.nd}` : ''
const xq = this.xqdcLabel(s.xqdc)
return `${nd}${xq}`
},
semesterWeeks() {
const s = this.safeSemester
if (!s.kxrq || !s.jsrq) return '-'
const start = new Date(String(s.kxrq).slice(0, 10))
const end = new Date(String(s.jsrq).slice(0, 10))
const diff = Math.round((end - start) / (1000 * 60 * 60 * 24)) + 1
return diff > 0 ? Math.ceil(diff / 7) : '-'
},
/** 班历统计:课程数 / 总学时 / 总学分 */
summary() {
const totalXs = this.records.reduce((sum, r) => sum + (Number(r.xs) || 0), 0)
const totalXf = this.records.reduce((sum, r) => sum + (Number(r.xf) || 0), 0)
return { totalXs, totalXf }
},
editTitle() {
return this.editMode === 'edit' ? '编辑课程任务' : '新增课程任务'
}
},
watch: {
visible(val) {
if (val) {
this.clipboardCount = this.readClipboard().length
this.loadData()
}
}
},
methods: {
/* ---------- 格式化 ---------- */
fmtDate(val) {
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学期'
if (val === 2 || val === '2') return '第2学期'
if (val === 3 || val === '3') return '第3学期'
return val !== undefined && val !== null && val !== '' ? `${val}学期` : '-'
},
cleanPayload(obj) {
const payload = {}
Object.keys(obj).forEach(key => {
const value = obj[key]
if (value !== '' && value !== null && value !== undefined) {
payload[key] = value
}
})
return payload
},
/* ---------- 列表加载 ---------- */
loadData() {
const s = this.semester || {}
if (!s.bh) return
this.loading = true
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 exact = {}
const byKbh = {}
plans.forEach(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 = 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 = this.findPlan(row)
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
},
/* ---------- 新增 / 编辑 ---------- */
createEmptyEditForm() {
const s = this.semester || {}
return {
bh: '',
nd: s.nd,
xydbh: s.xydbh || '',
xqdc: s.xqdc,
xydxqbh: s.bh || '',
jc: '',
kbh: '',
klx: '',
kcxh: undefined,
jhkcxh: undefined,
xhbs: undefined,
jybh: '',
jsbh: '',
rs: undefined,
zks: undefined,
xs: undefined,
llxs: undefined,
sjxs: undefined,
xf: undefined,
ksks: undefined,
ksbh: '',
ksdd: '',
cjfz: '',
bjrxypjf: undefined,
ksksbxs: undefined,
jysdh: '',
jysjhjybh: '',
jysjhbz: '',
pdxh: undefined,
pdqsz: undefined,
pdaz: undefined,
pdzzksj: undefined,
pdbz: undefined,
pdtbykxxbz: undefined,
pdqyzdyxs: undefined,
pdzdyxsfbsj: '',
bz2: undefined,
bz: ''
}
},
handleAdd() {
this.editMode = 'add'
this.editForm = this.createEmptyEditForm()
this.editVisible = true
this.$nextTick(() => {
if (this.$refs.editFormRef) this.$refs.editFormRef.clearValidate()
})
},
handleEdit(row) {
this.editMode = 'edit'
const s = this.semester || {}
const form = this.createEmptyEditForm()
Object.keys(form).forEach(key => {
if (row[key] !== undefined && row[key] !== null) form[key] = row[key]
})
form.xydxqbh = s.bh || row.xydxqbh
this.editForm = form
this.editVisible = true
this.$nextTick(() => {
if (this.$refs.editFormRef) this.$refs.editFormRef.clearValidate()
})
},
handleEditSubmit() {
this.$refs.editFormRef.validate(valid => {
if (!valid) return
const payload = this.cleanPayload({ ...this.editForm })
const req = this.editMode === 'edit'
? updateClassCalendar(payload)
: addTeamTask(payload)
this.saving = true
req.then(() => {
this.saving = false
this.editVisible = false
this.$message.success(this.editMode === 'edit' ? '课程任务已更新' : '课程任务已新增')
this.loadData()
}).catch(() => {
this.saving = false
})
})
},
/* ---------- 删除 ---------- */
handleDelete(row) {
this.$confirm(`确定删除该课程任务(${row.jc || row.bh})吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delTeamTask(row.bh).then(() => {
this.$message.success('已删除')
this.loadData()
})
}).catch(() => {})
},
/* ---------- 自动生成必修课程 ---------- */
handleAutoGenerate() {
const s = this.semester || {}
if (!s.bh || !s.xydbh || !s.xqdc) {
this.$message.warning('当前班次学期缺少学员队编号或学期第次,无法自动生成')
return
}
this.$confirm('将按专业教学计划自动生成该班次的必修课程(已存在的课程不会重复添加),是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 传班次学期编号:同一学员队在多个年度可能有学期第次相同的班次学期,
// 只用 (学员队编号, 学期第次) 会写进别的学年。
autoGenerateRequiredCourses(s.bh).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('请先勾选要应用的课程任务(选定区域)')
return
}
this.applyMode = 'region'
this.copyNote = `将选中的 ${bhs.length} 条课程任务复制到所选班次`
this.teamSelectVisible = true
},
handleApplyWhole() {
this.applyMode = 'whole'
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({
sourceXydxqbh: this.semester.bh,
targetBhList: targetBhList
}).then(res => {
const data = (res && res.data) || {}
this.$message.success(`已复制整个班次教学任务,共新增 ${data.count || 0} 条记录`)
this.loadData()
})
} else {
batchCopyCalendar({
sourceBhList: this.selection.map(r => r.bh),
targetBhList: targetBhList
}).then(res => {
const data = (res && res.data) || {}
this.$message.success(`已复制选定区域,共新增 ${data.count || 0} 条记录`)
this.loadData()
})
}
},
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('已刷新班次教学任务')
}
}
}
</script>
<style scoped lang="scss">
.class-calendar {
// ========== 1. 学期与班次信息 ==========
.info-panel {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px 20px;
margin-bottom: 12px;
.info-title-row {
display: flex;
align-items: baseline;
gap: 20px;
margin-bottom: 8px;
.semester-name,
.class-name {
font-size: 20px;
font-weight: 700;
color: #1e6fff;
}
}
.date-range-row {
display: flex;
flex-direction: column;
gap: 4px;
.range-item {
font-size: 13px;
font-weight: 600;
color: #1e6fff;
}
}
}
// ========== 2. 操作按钮区域 ==========
.settings-panel {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px 20px;
margin-bottom: 12px;
.action-buttons-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
}
// ========== 3. 表格区域 ==========
.table-area {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
.select-tip {
padding: 6px 12px;
font-size: 12px;
color: #909399;
border-top: 1px solid #ebeef5;
background: #fafafa;
}
}
.text-danger {
color: #f56c6c;
}
}
::v-deep .plan-mismatch-row > td {
background: #fde2e2 !important;
}
// ========== 弹窗内表单分区标题 ==========
.edit-form {
.form-section-title {
font-size: 13px;
font-weight: 600;
color: #303133;
margin: 6px 0 10px;
padding: 6px 10px;
background: #f0f7ff;
border-left: 3px solid #1e6fff;
}
}
</style>