Files
education/frontend/src/views/teachBusiness/timetableAdjust/index.vue
T

940 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>
<div class="page-container">
<!-- 统计卡片 -->
<div class="section-card">
<div class="stat-grid">
<div
v-for="card in statCards"
:key="card.title"
class="stat-card"
:style="{ background: card.gradient }"
tabindex="0"
role="button"
@click="handleStatClick(card)"
@keydown.enter="handleStatClick(card)"
>
<span class="stat-text">{{ card.title }}</span>
<span class="stat-value">{{ card.value }}</span>
</div>
</div>
</div>
<!-- 查询条件 -->
<div class="section-card">
<el-form :model="queryParams" :inline="true" size="small" class="search-form" @submit.native.prevent>
<el-form-item label="年度">
<el-select v-model="queryParams.nd" placeholder="请选择年度" clearable style="width: 140px">
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" />
</el-select>
</el-form-item>
<el-form-item label="申请教员编号">
<el-input v-model="queryParams.sqjybh" placeholder="请输入申请教员编号" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="教研室审批状态">
<el-select v-model="queryParams.jyspzzt" placeholder="请选择" clearable style="width: 150px">
<el-option v-for="item in auditStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 调课调整申请列表 -->
<div class="section-card">
<div class="section-header">
<div class="section-title">调停补课申请列表</div>
<div class="header-actions">
<el-button type="primary" plain icon="el-icon-plus" size="small" @click="openAdd">新增申请</el-button>
</div>
</div>
<el-table :data="tableData" v-loading="loading" size="small" border stripe class="compact-table" max-height="520">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column prop="kcmc" label="课程名称" width="200" align="center" show-overflow-tooltip />
<el-table-column label="申请教员" width="150" align="center" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.sqjyxm || scope.row.sqjybh || '-' }}</template>
</el-table-column>
<el-table-column prop="sy" label="调课事由" width="230" show-overflow-tooltip header-align="center" />
<el-table-column label="拟调整时间" width="200" align="center" show-overflow-tooltip>
<template slot-scope="scope">{{ fmtRqJc(scope.row.rq, scope.row.jc) }}</template>
</el-table-column>
<el-table-column prop="nd" label="年度" width="150" align="center" />
<el-table-column label="教研室审批状态" width="150" align="center">
<template slot-scope="scope">
<el-tag :type="auditStatusTag(scope.row.jyspzzt).type" size="mini">{{ auditStatusTag(scope.row.jyspzzt).label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="教研室查收状态" width="150" align="center">
<template slot-scope="scope">
<el-tag :type="receiveStatusTag(scope.row.jyscszt).type" size="mini">{{ receiveStatusTag(scope.row.jyscszt).label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="150" align="center">
<template slot-scope="scope">{{ fmtDateTime(scope.row.cjsj) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template slot-scope="scope">
<div class="ops-cell">
<el-button type="text" size="mini" icon="el-icon-view" @click="openDetail(scope.row)">详情</el-button>
<el-button v-if="showAuditMenu(scope.row)" type="text" size="mini" @click="openAudit(scope.row)">审批</el-button>
<el-button v-if="scope.row.sczt === 0 && scope.row.jyspzzt !== 1 && scope.row.jyspzzt !== 3" type="text" size="mini" class="danger-btn" @click="handleCancel(scope.row)">撤销</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-empty v-show="!loading && tableData.length === 0" description="暂无调整申请" />
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="loadList"
/>
</div>
<!-- 新增申请弹窗 -->
<el-dialog title="新增课表调整申请" :visible.sync="addVisible" width="640px" append-to-body>
<el-form ref="addForm" :model="addForm" :rules="addRules" label-width="110px">
<el-form-item label="年度" prop="nd">
<el-select v-model="addForm.nd" placeholder="请选择年度" style="width: 100%" @change="handleAddYearChange">
<el-option v-for="y in yearOptions" :key="y" :label="y" :value="y" />
</el-select>
</el-form-item>
<el-form-item label="课次编号" prop="sskcbbh">
<el-input v-model="addForm.sskcbbh" placeholder="请从可供调课课次中选择" disabled />
<el-button type="primary" plain size="mini" class="pick-lesson-btn" :loading="lessonLoading" @click="loadAdjustableLessons">选择可供调课课次</el-button>
</el-form-item>
<el-form-item label="调课事由" prop="sy">
<el-input
v-model="addForm.sy"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入调课事由"
/>
</el-form-item>
<el-form-item label="拟调整日期" prop="rq">
<el-date-picker v-model="addForm.rq" type="date" value-format="yyyy-MM-dd" placeholder="请选择日期" style="width: 100%" />
</el-form-item>
<el-form-item label="拟调整节次" prop="jc">
<el-select v-model="addForm.jc" placeholder="请选择节次" style="width: 100%">
<el-option v-for="n in jcOptions" :key="n" :label="jcLabel(n)" :value="n" />
</el-select>
</el-form-item>
<el-form-item label="申请教员">
<el-input v-model="addForm.sqjybh" placeholder="由服务端根据当前登录人写入" disabled />
</el-form-item>
<el-form-item label="学员队名称">
<el-input v-model="addForm.xydmc" placeholder="选择可供调课课次后自动带出" disabled />
</el-form-item>
<el-form-item label="原教室编号">
<el-input v-model="addForm.yjsbh" placeholder="选择可供调课课次后自动带出" disabled />
</el-form-item>
<el-form-item label="拟调整教室编号">
<el-select
v-model="addForm.xjsbh"
placeholder="请选择拟调整教室"
clearable
filterable
:loading="classroomLoading"
style="width: 100%"
>
<el-option
v-for="room in classroomOptions"
:key="room.jsbh"
:label="classroomOptionLabel(room)"
:value="room.jsbh"
/>
</el-select>
</el-form-item>
<el-form-item label="原上课时间">
<el-input v-model="addForm.ydsjap" placeholder="选择可供调课课次后自动带出" disabled />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="addLoading" @click="submitAdd">提交申请</el-button>
<el-button @click="addVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 可供调课课次选择弹窗 -->
<el-dialog title="选择可供调课课次" :visible.sync="lessonDialogVisible" width="760px" append-to-body>
<el-table :data="lessonOptions" v-loading="lessonLoading" size="small" border stripe max-height="420">
<el-table-column prop="kcmc" label="课程名称" min-width="140" align="center" show-overflow-tooltip />
<el-table-column label="原上课时间" width="160" align="center">
<template slot-scope="scope">{{ fmtRqJc(scope.row.rq, scope.row.jc) }}</template>
</el-table-column>
<el-table-column prop="xydmc" label="学员队" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="kcxh" label="课次序号" width="90" align="center" />
<el-table-column prop="jsbh" label="教室编号" width="110" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="90" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="pickLesson(scope.row)">选择</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-show="!lessonLoading && lessonOptions.length === 0" description="暂无可供调课课次" />
</el-dialog>
<!-- 审批弹窗 -->
<el-dialog title="教研室审批" :visible.sync="auditVisible" width="520px" append-to-body>
<el-form ref="auditForm" :model="auditForm" label-width="100px">
<el-form-item label="审批结果" required>
<el-radio-group v-model="auditForm.spzt">
<el-radio :label="1">同意</el-radio>
<el-radio :label="2">发回</el-radio>
<el-radio :label="3">拒绝</el-radio>
</el-radio-group>
<div class="form-tip">点审批即已查收</div>
</el-form-item>
<el-form-item label="审批意见">
<el-input v-model="auditForm.fhyj" type="textarea" :rows="3" placeholder="请输入审批意见(发回/拒绝时必填)" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="auditLoading" @click="submitAudit">确定</el-button>
<el-button @click="auditVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 查看详情弹窗 -->
<el-dialog title="申请详情" :visible.sync="detailVisible" width="760px" append-to-body>
<div v-loading="detailLoading">
<el-descriptions v-if="detailData" :column="2" border size="small">
<el-descriptions-item label="课程名称">{{ detailData.kcmc || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请教员">{{ detailData.sqjyxm || detailData.sqjybh || '-' }}</el-descriptions-item>
<el-descriptions-item label="调课事由" :span="2">{{ detailData.sy || '-' }}</el-descriptions-item>
<el-descriptions-item label="原上课时间">{{ detailData.ydsjap || '-' }}</el-descriptions-item>
<el-descriptions-item label="拟调整时间">{{ fmtRqJc(detailData.rq, detailData.jc) }}</el-descriptions-item>
<el-descriptions-item label="年度">{{ detailData.nd || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ fmtDateTime(detailData.cjsj) }}</el-descriptions-item>
<el-descriptions-item label="教研室审批状态">
<el-tag :type="auditStatusTag(detailData.jyspzzt).type" size="mini">{{ auditStatusTag(detailData.jyspzzt).label }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="教研室查收状态">
<el-tag :type="receiveStatusTag(detailData.jyscszt).type" size="mini">{{ receiveStatusTag(detailData.jyscszt).label }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="教研室审批人">{{ detailData.jyspzrbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="教研室审批时间">{{ fmtDateTime(detailData.jyspzsj) }}</el-descriptions-item>
<el-descriptions-item label="审批意见" :span="2">{{ detailData.fhyj || '-' }}</el-descriptions-item>
<el-descriptions-item label="教研室查收人">{{ detailData.jyscsrbh || '-' }}</el-descriptions-item>
<el-descriptions-item label="教研室查收时间">{{ fmtDateTime(detailData.jyscssj) }}</el-descriptions-item>
<el-descriptions-item label="教学内容" :span="2">{{ detailData.jxnr || '-' }}</el-descriptions-item>
<el-descriptions-item label="教学要点" :span="2">{{ detailData.jxyd || '-' }}</el-descriptions-item>
<el-descriptions-item label="教学方法" :span="2">{{ detailData.jxff || '-' }}</el-descriptions-item>
<el-descriptions-item label="教学保障备注" :span="2">{{ detailData.jxbzbz || '-' }}</el-descriptions-item>
<el-descriptions-item label="用车信息" :span="2">{{ detailData.ycxx || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailData.bz || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 新教室 -->
<div v-if="detailData && detailData.roomList && detailData.roomList.length" class="sub-section">
<div class="sub-title">新教室</div>
<el-table :data="detailData.roomList" size="small" border stripe>
<el-table-column prop="jsbh" label="教室编号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="jsmc" label="教室名称" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="bz" label="备注" min-width="160" show-overflow-tooltip header-align="center" />
</el-table>
</div>
<!-- 新辅助教员 -->
<div v-if="detailData && detailData.teacherList && detailData.teacherList.length" class="sub-section">
<div class="sub-title">新辅助教员</div>
<el-table :data="detailData.teacherList" size="small" border stripe>
<el-table-column prop="fzjybh" label="教员编号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="fzjyxm" label="教员姓名" min-width="120" align="center" show-overflow-tooltip />
<el-table-column label="是否主教员" width="100" align="center">
<template slot-scope="scope">{{ scope.row.zjy === 1 ? '是' : '否' }}</template>
</el-table-column>
<el-table-column prop="bz" label="备注" min-width="140" show-overflow-tooltip header-align="center" />
</el-table>
</div>
<!-- 新学员队 -->
<div v-if="detailData && detailData.teamList && detailData.teamList.length" class="sub-section">
<div class="sub-title">新学员队</div>
<el-table :data="detailData.teamList" size="small" border stripe>
<el-table-column prop="xydbh" label="学员队编号" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="xydmc" label="学员队名称" min-width="140" align="center" show-overflow-tooltip />
</el-table>
</div>
<!-- 新保障明细 -->
<div v-if="detailData && detailData.supportList && detailData.supportList.length" class="sub-section">
<div class="sub-title">新保障明细</div>
<el-table :data="detailData.supportList" size="small" border stripe>
<el-table-column prop="bztgmxbh" label="保障编号" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="bztgmxmc" label="保障名称" min-width="160" align="center" show-overflow-tooltip />
<el-table-column prop="sl" label="数量" width="80" align="center" />
<el-table-column prop="bz" label="备注" min-width="140" show-overflow-tooltip header-align="center" />
</el-table>
</div>
<!-- 机关审批记录 -->
<div v-if="detailData && detailData.auditList && detailData.auditList.length" class="sub-section">
<div class="sub-title">机关审批记录</div>
<el-table :data="detailData.auditList" size="small" border stripe>
<el-table-column prop="jgmc" label="机关" min-width="140" align="center" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.jgmc || scope.row.jgbh || '-' }}</template>
</el-table-column>
<el-table-column label="审批状态" width="100" align="center">
<template slot-scope="scope">
<el-tag :type="auditStatusTag(scope.row.spzt).type" size="mini">{{ auditStatusTag(scope.row.spzt).label }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="sprxm" label="审批人" min-width="110" align="center" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.sprxm || scope.row.sprbh || '-' }}</template>
</el-table-column>
<el-table-column label="审批时间" width="150" align="center">
<template slot-scope="scope">{{ fmtDateTime(scope.row.spsj) }}</template>
</el-table-column>
<el-table-column prop="fhyj" label="发回意见" min-width="140" show-overflow-tooltip header-align="center" />
</el-table>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listScheduleAdjust,
listAdjustableLessons,
getScheduleAdjustDetail,
submitScheduleAdjust,
auditByTeachingOffice,
cancelScheduleAdjust
} from '@/api/teachBusiness/courseRunning'
import { listAllSemester } from '@/api/teachBusiness/semester'
import { listAllClassroom } from '@/api/teachBusiness/classroom'
import { mapGetters } from 'vuex'
// 节次 -> 节次区间映射
const JC_SECTION = {
1: '1-2节', 2: '3-4节', 3: '5-6节', 4: '7-8节', 5: '9-10节', 6: '11-12节'
}
// 审批状态:0-未审批,1-已同意,2-已发回,3-已拒绝
const AUDIT_STATUS = {
0: { label: '未审批', type: 'info' },
1: { label: '已同意', type: 'success' },
2: { label: '已发回', type: 'warning' },
3: { label: '已拒绝', type: 'danger' }
}
// 查收状态:0-未查收,1-已查收
const RECEIVE_STATUS = {
0: { label: '未查收', type: 'info' },
1: { label: '已查收', type: 'success' }
}
export default {
name: 'TimetableAdjust',
data() {
return {
loading: false,
tableData: [],
total: 0,
queryParams: {
nd: undefined,
sqjybh: undefined,
jyspzzt: undefined,
pageNum: 1,
pageSize: 20
},
// 年度下拉数据来自 /semester/all(真实后端数据)
yearOptions: [],
defaultNd: undefined,
jcOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
auditStatusOptions: [
{ label: '未审批', value: 0 },
{ label: '已同意', value: 1 },
{ label: '已发回', value: 2 },
{ label: '已拒绝', value: 3 }
],
statCards: this.createStatCards([0, 0, 0, 0]),
// 新增申请
addVisible: false,
addLoading: false,
addForm: {},
addRules: {
nd: [{ required: true, message: '请选择年度', trigger: 'change' }],
sskcbbh: [{ required: true, message: '请选择可供调课课次', trigger: 'change' }],
sy: [{ required: true, message: '请输入调课事由', trigger: 'blur' }],
rq: [{ required: true, message: '请选择拟调整日期', trigger: 'change' }],
jc: [{ required: true, message: '请选择拟调整节次', trigger: 'change' }]
},
// 可供调课课次
lessonDialogVisible: false,
lessonLoading: false,
lessonOptions: [],
classroomLoading: false,
classroomOptions: [],
// 审批
auditVisible: false,
auditLoading: false,
auditForm: { ssdksqbh: '', spzt: 1, fhyj: '' },
// 详情
detailVisible: false,
detailLoading: false,
detailData: null
}
},
computed: {
...mapGetters(['roles']),
isTeacher() {
const r = this.roles || []
const hasTeacher = r.includes('TEACHER') || r.includes('teacher') || r.includes('ROLE_TEACHER')
const hasAdmin = r.includes('admin') || r.includes('ROLE_ADMIN')
return hasTeacher && !hasAdmin
}
},
created() {
this.loadYearOptions().then(() => {
this.loadStatistics()
this.loadList()
})
},
methods: {
/* ---------- 年度下拉(数据来自 /semester/all ---------- */
loadYearOptions() {
return listAllSemester().then(response => {
const list = response.data || []
// 去重并按年度倒序
const map = {}
list.forEach(item => {
if (item.nd !== null && item.nd !== undefined && item.nd !== '') map[item.nd] = item
})
const arr = Object.keys(map).sort((a, b) => String(b).localeCompare(String(a)))
this.yearOptions = arr
// 默认年度:优先当前学期(dqxq=true),否则取最新年度
const current = list.find(item => item.dqxq === true)
this.defaultNd = (current && current.nd) || arr[0]
if (!this.queryParams.nd) this.queryParams.nd = this.defaultNd
return arr
}).catch(() => {
this.yearOptions = []
this.defaultNd = undefined
return []
})
},
/* ---------- 通用格式化 ---------- */
fmtDateTime(v) {
if (!v) return '-'
return String(v).replace('T', ' ').substring(0, 16)
},
fmtRqJc(rq, jc) {
if (!rq) return '-'
const date = String(rq).substring(0, 10)
const sec = jc === null || jc === undefined || jc === '' ? '第?节' : (JC_SECTION[jc] || '第' + jc + '节')
return date + ' ' + sec
},
jcLabel(n) {
return JC_SECTION[n] || ('第' + n + '节')
},
auditStatusTag(v) {
return AUDIT_STATUS[v] || { label: '-', type: 'info' }
},
receiveStatusTag(v) {
return RECEIVE_STATUS[v] || { label: '-', type: 'info' }
},
/* ---------- 列表查询 ---------- */
loadList() {
const params = {
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize
}
if (this.queryParams.nd) params.nd = this.queryParams.nd
if (this.queryParams.sqjybh) params.sqjybh = this.queryParams.sqjybh
if (this.queryParams.jyspzzt !== undefined && this.queryParams.jyspzzt !== '') {
params.jyspzzt = this.queryParams.jyspzzt
}
this.loading = true
listScheduleAdjust(params).then(response => {
const data = response.data || {}
this.tableData = data.records || []
this.total = data.total || 0
this.loading = false
}).catch(() => {
this.tableData = []
this.total = 0
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.loadList()
},
resetQuery() {
this.queryParams = {
nd: this.defaultNd,
sqjybh: undefined,
jyspzzt: undefined,
pageNum: 1,
pageSize: 20
}
this.loadList()
},
loadStatistics() {
const baseParams = {
nd: this.defaultNd,
pageNum: 1,
pageSize: 1
}
const statusFilters = [undefined, 0, 1, 3]
const requests = statusFilters.map(status => {
const params = Object.assign({}, baseParams)
if (status !== undefined) {
params.jyspzzt = status
}
return listScheduleAdjust(params)
.then(response => Number((response.data && response.data.total) || 0))
.catch(() => 0)
})
return Promise.all(requests).then(counts => {
this.statCards = this.createStatCards(counts)
})
},
createStatCards(counts) {
return [
{
title: '本页申请',
value: counts[0],
gradient: 'linear-gradient(135deg, #00875a 0%, #00b877 100%)',
filter: undefined
},
{
title: '教研室待审批',
value: counts[1],
gradient: 'linear-gradient(135deg, #e6a23c 0%, #f8b04c 100%)',
filter: 0
},
{
title: '教研室已同意',
value: counts[2],
gradient: 'linear-gradient(135deg, #409eff 0%, #66b1ff 100%)',
filter: 1
},
{
title: '教研室已拒绝',
value: counts[3],
gradient: 'linear-gradient(135deg, #f56c6c 0%, #f78989 100%)',
filter: 3
}
]
},
handleStatClick(card) {
this.queryParams.jyspzzt = card.filter
this.handleQuery()
},
/* ---------- 新增申请 ---------- */
openAdd() {
this.addForm = {
nd: this.defaultNd || this.yearOptions[0],
sskcbbh: '',
sy: '',
rq: '',
jc: 1,
sqjybh: '',
xydrwbh: '',
kbh: '',
kcbbh: '',
xydmc: '',
yjsbh: '',
xjsbh: '',
ydsjap: '',
jxnr: '',
jxyd: '',
jxff: '',
jxbzbz: '',
ycxx: '',
bz: '',
roomList: [],
teacherList: [],
teamList: [],
supportList: []
}
this.lessonOptions = []
this.loadClassroomOptions()
this.$nextTick(() => this.$refs.addForm && this.$refs.addForm.clearValidate())
this.addVisible = true
},
handleAddYearChange() {
Object.assign(this.addForm, {
sskcbbh: '',
rq: '',
jc: 1,
sqjybh: '',
xydrwbh: '',
kbh: '',
kcbbh: '',
xydmc: '',
yjsbh: '',
xjsbh: '',
ydsjap: '',
jxnr: '',
jxyd: '',
jxff: '',
jxbzbz: '',
ycxx: '',
bz: '',
roomList: [],
teacherList: [],
teamList: [],
supportList: []
})
this.lessonOptions = []
},
loadClassroomOptions() {
this.classroomLoading = true
return listAllClassroom().then(response => {
const classrooms = Array.isArray(response.data) ? response.data : []
this.classroomOptions = classrooms
.filter(room => room.jsbh && !this.isDisabledValue(room.ty))
.sort((first, second) => String(first.jsbh).localeCompare(String(second.jsbh), 'zh-CN'))
}).catch(() => {
this.classroomOptions = []
this.$message.warning('教室列表加载失败,请稍后重试')
}).finally(() => {
this.classroomLoading = false
})
},
classroomOptionLabel(room) {
return room.jsmc ? room.jsbh + ' · ' + room.jsmc : room.jsbh
},
isDisabledValue(value) {
return value === true || value === 1 || value === '1' || value === 'true'
},
loadAdjustableLessons() {
if (!this.addForm.nd) {
this.$message.warning('请先选择年度')
return
}
this.lessonLoading = true
listAdjustableLessons({ nd: this.addForm.nd }).then(response => {
const data = response.data || []
this.lessonOptions = Array.isArray(data) ? data : (data.records || [])
this.lessonLoading = false
this.lessonDialogVisible = true
}).catch(() => {
this.lessonLoading = false
this.$message.warning('加载可供调课课次失败,请稍后重试')
})
},
pickLesson(row) {
const lessonNumber = row.sskcbbh || row.bh || ''
if (!lessonNumber) {
this.$message.warning('当前课次缺少课次编号,无法选择')
return
}
const currentRoomNumber = row.jsbh || row.yjsbh || row.xjsbh || ''
const roomList = this.normalizeRoomList(row.roomList, currentRoomNumber)
const teamList = this.normalizeTeamList(row.teamList, row.xydbh)
Object.assign(this.addForm, {
sskcbbh: lessonNumber,
rq: row.rq ? String(row.rq).substring(0, 10) : '',
jc: row.jc === null || row.jc === undefined ? 1 : Number(row.jc),
nd: row.nd || this.addForm.nd,
sqjybh: row.sqjybh || '',
xydrwbh: row.xydrwbh || '',
kbh: row.kbh || '',
kcbbh: row.kcbbh || '',
xydmc: row.xydmc || '',
yjsbh: row.yjsbh || currentRoomNumber,
xjsbh: row.xjsbh || currentRoomNumber,
ydsjap: row.rq ? this.fmtRqJc(row.rq, row.jc) : (row.ydsjap || ''),
jxnr: row.jxnr || '',
jxyd: row.jxyd || '',
jxff: row.jxff || '',
jxbzbz: row.jxbzbz || '',
ycxx: row.ycxx || '',
bz: row.bz || '',
roomList: roomList,
teacherList: this.normalizeTeacherList(row.teacherList),
teamList: teamList,
supportList: this.normalizeSupportList(row.supportList)
})
;['jysdh', 'kcmc', 'sqjyxm'].forEach(key => {
if (row[key] !== undefined && row[key] !== null) this.addForm[key] = row[key]
})
this.lessonDialogVisible = false
this.$nextTick(() => this.$refs.addForm && this.$refs.addForm.validateField('sskcbbh'))
this.$message.success('已选择课次:' + (row.kcmc || lessonNumber))
},
submitAdd() {
this.$refs.addForm.validate(valid => {
if (!valid) return
const payload = {
sskcbbh: this.addForm.sskcbbh,
sy: this.addForm.sy,
rq: this.addForm.rq + 'T00:00:00',
jc: this.addForm.jc,
nd: this.addForm.nd,
roomList: this.buildSubmitRoomList(),
teacherList: this.normalizeTeacherList(this.addForm.teacherList),
teamList: this.normalizeTeamList(this.addForm.teamList),
supportList: this.normalizeSupportList(this.addForm.supportList)
}
const optionalFields = [
'sqjybh', 'xydrwbh', 'kbh', 'kcbbh', 'xjsbh', 'jxnr', 'jxyd',
'jxff', 'jxbzbz', 'ycxx', 'bz'
]
optionalFields.forEach(key => {
if (this.addForm[key]) payload[key] = this.addForm[key]
})
this.addLoading = true
submitScheduleAdjust(payload).then(() => {
this.addLoading = false
this.addVisible = false
this.$message.success('申请提交成功')
this.loadList()
}).catch(() => {
this.addLoading = false
})
})
},
normalizeRoomList(roomList, fallbackRoomNumber) {
const normalizedList = (Array.isArray(roomList) ? roomList : [])
.filter(item => item && item.jsbh)
.map(item => ({
jsbh: item.jsbh,
bz: item.bz || ''
}))
if (normalizedList.length === 0 && fallbackRoomNumber) {
normalizedList.push({ jsbh: fallbackRoomNumber, bz: '' })
}
return normalizedList
},
normalizeTeacherList(teacherList) {
return (Array.isArray(teacherList) ? teacherList : [])
.filter(item => item && item.fzjybh)
.map(item => ({ fzjybh: item.fzjybh }))
},
normalizeTeamList(teamList, fallbackTeamNumber) {
const normalizedList = (Array.isArray(teamList) ? teamList : [])
.filter(item => item && item.xydbh)
.map(item => ({ xydbh: item.xydbh }))
if (normalizedList.length === 0 && fallbackTeamNumber) {
normalizedList.push({ xydbh: fallbackTeamNumber })
}
return normalizedList
},
normalizeSupportList(supportList) {
return (Array.isArray(supportList) ? supportList : [])
.filter(item => item && item.bztgmxbh)
.map(item => ({
bztgmxbh: item.bztgmxbh,
sl: item.sl === null || item.sl === undefined ? undefined : Number(item.sl)
}))
},
buildSubmitRoomList() {
if (!this.addForm.xjsbh) return []
const currentRoomList = this.normalizeRoomList(this.addForm.roomList)
if (this.addForm.xjsbh === this.addForm.yjsbh && currentRoomList.length > 0) {
return currentRoomList
}
const currentRoom = currentRoomList.find(item => item.jsbh === this.addForm.xjsbh)
return [{
jsbh: this.addForm.xjsbh,
bz: currentRoom ? currentRoom.bz : ''
}]
},
/* ---------- 审批 ---------- */
showAuditMenu(row) {
// 教员角色不展示审批入口;已拒绝状态不展示审批
if (this.isTeacher) return false
return row.sczt === 0 && (row.jyspzzt === 0 || row.jyspzzt === 2)
},
openAudit(row) {
this.auditForm = {
ssdksqbh: row.ssdksqbh,
spzt: 1,
fhyj: ''
}
this.auditVisible = true
},
submitAudit() {
const f = this.auditForm
if (f.spzt !== 1 && !f.fhyj) {
this.$message.warning('发回/拒绝时请填写审批意见')
return
}
const payload = {
ssdksqbh: f.ssdksqbh,
spzt: f.spzt,
fhyj: f.fhyj
}
this.auditLoading = true
auditByTeachingOffice(payload).then(() => {
this.auditLoading = false
this.auditVisible = false
this.$message.success('审批成功')
this.loadList()
}).catch(() => {
this.auditLoading = false
})
},
/* ---------- 撤销 ---------- */
handleCancel(row) {
this.$confirm('确认撤销该调课申请?', '提示', { type: 'warning' })
.then(() => {
cancelScheduleAdjust({ ssdksqbh: row.ssdksqbh }).then(() => {
this.$message.success('撤销成功')
this.loadList()
}).catch(() => {})
})
.catch(() => {})
},
/* ---------- 详情 ---------- */
openDetail(row) {
this.detailVisible = true
this.detailLoading = true
this.detailData = null
getScheduleAdjustDetail(row.ssdksqbh).then(response => {
this.detailData = response.data || {}
this.detailLoading = false
}).catch(() => {
this.detailData = null
this.detailLoading = false
})
}
}
}
</script>
<style scoped lang="scss">
.page-container {
padding: 16px;
.section-card {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 16px 20px;
margin-bottom: 16px;
.section-title {
font-size: 16px;
font-weight: 700;
color: #303133;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
}
}
/* 统计卡片 */
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-top: 14px;
}
.stat-card {
display: flex;
flex-direction: column;
justify-content: center;
padding: 16px 20px;
border-radius: 6px;
color: #fff;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.stat-text {
font-size: 13px;
opacity: 0.92;
}
.stat-value {
margin-top: 8px;
font-size: 26px;
font-weight: 700;
line-height: 1;
}
}
/* 查询区域 */
.search-form {
margin-bottom: -16px;
::v-deep .el-form-item {
margin-bottom: 16px;
}
}
.compact-table {
width: 100%;
}
.danger-btn {
color: #f56c6c;
&:hover {
color: #f78989;
}
}
.ops-cell {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0 4px;
}
.form-tip {
font-size: 12px;
color: #909399;
line-height: 1.4;
margin-top: 4px;
}
.pick-lesson-btn {
margin-top: 6px;
width: 100%;
}
.sub-section {
margin-top: 16px;
.sub-title {
font-size: 14px;
font-weight: 700;
color: #303133;
margin-bottom: 8px;
}
}
}
@media (max-width: 992px) {
.page-container .stat-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>