forked from liweijie/education
425 lines
13 KiB
Vue
425 lines
13 KiB
Vue
<template>
|
||
<div class="app-container conflict-page">
|
||
<!-- 冲突检查区域 -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<div>
|
||
<div class="section-title">教学资源冲突检查</div>
|
||
<div class="section-desc">当前检查年度:{{ nd ? nd + ' 年' : '未选择' }},可单独检查或执行全部检查</div>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-form inline class="toolbar-form" @submit.native.prevent>
|
||
<el-form-item label="年度" class="year-item">
|
||
<el-select
|
||
v-model="nd"
|
||
placeholder="请选择年度"
|
||
filterable
|
||
style="width: 130px"
|
||
@change="handleNdChange"
|
||
>
|
||
<el-option v-for="y in yearOptions" :key="y" :label="y + ' 年'" :value="y" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-button size="mini" :disabled="loading" @click="handleReset">重置</el-button>
|
||
<el-button type="primary" size="mini" :loading="loading" @click="handleCheckAll">执行全部检查</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 检查项卡片网格 -->
|
||
<div class="check-grid">
|
||
<div
|
||
v-for="item in conflictItems"
|
||
:key="item.key"
|
||
class="check-item-card"
|
||
:class="{ active: activeKey === item.key }"
|
||
@click="selectCard(item)"
|
||
>
|
||
<div class="check-item-header">
|
||
<span class="check-item-name">{{ item.label }}</span>
|
||
<el-tag :type="getStatus(item).type" size="mini">{{ getStatus(item).text }}</el-tag>
|
||
</div>
|
||
<div class="check-item-desc">{{ item.desc }}</div>
|
||
<div class="check-item-footer">
|
||
<span class="conflict-count">
|
||
冲突数:
|
||
<b :class="item.count > 0 ? 'danger-text' : 'normal-text'">{{ item.count }}</b>
|
||
</span>
|
||
<el-button type="primary" plain size="mini" :disabled="loading" @click.stop="handleCheck(item)">检查</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 冲突明细区域 -->
|
||
<div class="section-card">
|
||
<div class="section-header detail-header">
|
||
<div class="section-title">
|
||
冲突明细
|
||
<span v-if="activeKey" class="active-label">— {{ activeLabel }}</span>
|
||
</div>
|
||
<span v-if="activeKey" class="detail-count">共 {{ detailTotal }} 条记录</span>
|
||
</div>
|
||
<template v-if="activeKey">
|
||
<el-table v-loading="detailLoading" :data="activeDetails" border stripe size="mini" max-height="500">
|
||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||
<el-table-column label="冲突号" width="90" align="center" show-overflow-tooltip>
|
||
<template slot-scope="scope">{{ scope.row.conflictNo || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="courseNames" label="课程" min-width="150" align="center" show-overflow-tooltip />
|
||
<el-table-column label="日期" width="100" align="center">
|
||
<template slot-scope="scope">{{ fmtDate(scope.row.rq) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="jc" label="节次" width="70" align="center" />
|
||
<el-table-column prop="xydNames" label="班次" min-width="140" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="teacherNames" label="教员" min-width="120" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="classroomNames" label="场地" min-width="140" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="responsibleDept" label="责任单位" min-width="110" align="center" show-overflow-tooltip />
|
||
</el-table>
|
||
<el-pagination
|
||
v-if="detailTotal > 0"
|
||
class="detail-pagination"
|
||
background
|
||
layout="total, prev, pager, next"
|
||
:current-page="detailQuery.pageNum"
|
||
:page-size="detailQuery.pageSize"
|
||
:total="detailTotal"
|
||
@current-change="handlePageChange"
|
||
/>
|
||
<el-empty v-if="detailTotal === 0 && !detailLoading" description="暂无冲突明细" />
|
||
</template>
|
||
<el-empty v-else v-show="!loading" description="点击上方卡片「检查」按钮,在此查看冲突明细" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { checkConflict, checkAllConflict, resetConflict, getConflictDetails } from '@/api/teachBusiness/timetableConflict'
|
||
import { listAllSemester } from '@/api/teachBusiness/semester'
|
||
|
||
// 四类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量;
|
||
// 检查状态与冲突数完全来自后端接口)
|
||
// 注意:教员卡片含「教员历不可排」、教室卡片含「场地历不可用」,与排课窗口硬冲突口径一致
|
||
const CARD_DEFS = [
|
||
{ key: 'TEAM_CONFLICT', label: '教学班时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程' },
|
||
{ key: 'ELECTIVE_REQUIRED_CONFLICT', label: '教学班必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠' },
|
||
{ key: 'TEACHER_CONFLICT', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程,或教员历不可排课时段已有课程' },
|
||
{ key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用,或场地历不可用时段已有课程' }
|
||
]
|
||
|
||
export default {
|
||
name: 'TimetableConflict',
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
detailLoading: false,
|
||
// 年度(数据来自 /semester/all,真实后端数据)
|
||
nd: undefined,
|
||
yearOptions: [],
|
||
conflictItems: CARD_DEFS.map(c => ({ ...c, count: 0, checked: false })),
|
||
activeKey: '',
|
||
activeDetails: [],
|
||
detailTotal: 0,
|
||
detailQuery: { pageNum: 1, pageSize: 20 }
|
||
}
|
||
},
|
||
computed: {
|
||
activeLabel() {
|
||
const item = this.conflictItems.find(i => i.key === this.activeKey)
|
||
return item ? item.label : ''
|
||
}
|
||
},
|
||
created() {
|
||
this.loadYearOptions()
|
||
},
|
||
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.nd = (current && current.nd) || arr[0]
|
||
return arr
|
||
}).catch(() => {
|
||
this.yearOptions = []
|
||
this.nd = undefined
|
||
return []
|
||
})
|
||
},
|
||
fmtDate(rq) {
|
||
return (rq || '').substring(0, 10)
|
||
},
|
||
getStatus(item) {
|
||
if (!item.checked) return { type: 'info', text: '未检查' }
|
||
return item.count > 0
|
||
? { type: 'danger', text: '存在 ' + item.count + ' 处冲突' }
|
||
: { type: 'success', text: '无冲突' }
|
||
},
|
||
/* 用后端返回的单卡片结果同步对应卡片状态 */
|
||
applyCard(card) {
|
||
if (!card) return
|
||
const item = this.conflictItems.find(i => i.key === card.dimensionCode)
|
||
if (!item) return
|
||
item.checked = card.checked
|
||
item.count = card.conflictCount
|
||
},
|
||
applySummary(summary) {
|
||
if (!summary || !summary.cards) return
|
||
summary.cards.forEach(card => this.applyCard(card))
|
||
},
|
||
/* ---------- 单个检查 ---------- */
|
||
handleCheck(item) {
|
||
if (!this.nd) {
|
||
this.$message.warning('请先选择年度')
|
||
return
|
||
}
|
||
this.loading = true
|
||
checkConflict({ nd: this.nd, dimensionCode: item.key }).then(response => {
|
||
this.applyCard(response.data)
|
||
this.activeKey = item.key
|
||
this.detailQuery.pageNum = 1
|
||
this.loadDetails()
|
||
if (item.count > 0) {
|
||
this.$message.warning('「' + item.label + '」检查完成,发现 ' + item.count + ' 处冲突')
|
||
} else {
|
||
this.$message.success('「' + item.label + '」检查完成,未发现冲突')
|
||
}
|
||
}).finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
/* ---------- 全部检查 ---------- */
|
||
handleCheckAll() {
|
||
if (!this.nd) {
|
||
this.$message.warning('请先选择年度')
|
||
return
|
||
}
|
||
this.loading = true
|
||
checkAllConflict({ nd: this.nd }).then(response => {
|
||
const summary = response.data
|
||
this.applySummary(summary)
|
||
const total = summary ? summary.totalConflictCount : 0
|
||
this.$message.success('全部检查完成,共发现 ' + total + ' 处冲突')
|
||
const firstConflict = this.conflictItems.find(i => i.checked && i.count > 0)
|
||
const firstChecked = this.conflictItems.find(i => i.checked)
|
||
this.activeKey = firstConflict ? firstConflict.key : (firstChecked ? firstChecked.key : '')
|
||
this.detailQuery.pageNum = 1
|
||
if (this.activeKey) {
|
||
this.loadDetails()
|
||
} else {
|
||
this.activeDetails = []
|
||
this.detailTotal = 0
|
||
}
|
||
}).finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
/* ---------- 重置 ---------- */
|
||
handleReset() {
|
||
if (!this.nd) {
|
||
this.$message.warning('请先选择年度')
|
||
return
|
||
}
|
||
resetConflict({ nd: this.nd }).then(response => {
|
||
this.applySummary(response.data)
|
||
this.activeKey = ''
|
||
this.activeDetails = []
|
||
this.detailTotal = 0
|
||
this.$message.success('检查结果已重置')
|
||
})
|
||
},
|
||
/* 点击卡片:仅已检查的卡片可查看其明细 */
|
||
selectCard(item) {
|
||
if (!item.checked) return
|
||
this.activeKey = item.key
|
||
this.detailQuery.pageNum = 1
|
||
this.loadDetails()
|
||
},
|
||
/* ---------- 冲突明细 ---------- */
|
||
loadDetails() {
|
||
if (!this.activeKey) {
|
||
this.activeDetails = []
|
||
this.detailTotal = 0
|
||
return
|
||
}
|
||
this.detailLoading = true
|
||
getConflictDetails({
|
||
nd: this.nd,
|
||
dimensionCode: this.activeKey,
|
||
pageNum: this.detailQuery.pageNum,
|
||
pageSize: this.detailQuery.pageSize
|
||
}).then(response => {
|
||
const page = response.data || {}
|
||
this.activeDetails = page.records || []
|
||
this.detailTotal = page.total || 0
|
||
}).catch(() => {
|
||
this.activeDetails = []
|
||
this.detailTotal = 0
|
||
}).finally(() => {
|
||
this.detailLoading = false
|
||
})
|
||
},
|
||
handlePageChange(page) {
|
||
this.detailQuery.pageNum = page
|
||
this.loadDetails()
|
||
},
|
||
/* 切换年度:该年度检查状态未知,重置为未检查 */
|
||
handleNdChange() {
|
||
this.conflictItems.forEach(i => {
|
||
i.checked = false
|
||
i.count = 0
|
||
})
|
||
this.activeKey = ''
|
||
this.activeDetails = []
|
||
this.detailTotal = 0
|
||
this.detailQuery.pageNum = 1
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.conflict-page {
|
||
.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;
|
||
|
||
.toolbar-form {
|
||
margin-right: 4px;
|
||
|
||
.year-item {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.section-desc {
|
||
margin-top: 8px;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.detail-header {
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
|
||
/* 检查项卡片网格 */
|
||
.check-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.check-item-card {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 6px;
|
||
padding: 14px 16px;
|
||
transition: box-shadow 0.2s ease;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
&.active {
|
||
border-color: var(--edu-green-primary, #00875a);
|
||
box-shadow: 0 0 0 2px rgba(0, 135, 90, 0.15);
|
||
}
|
||
|
||
.check-item-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
|
||
.check-item-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
}
|
||
|
||
.check-item-desc {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.check-item-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 12px;
|
||
|
||
.conflict-count {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
|
||
b {
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.danger-text {
|
||
color: #f56c6c;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.normal-text {
|
||
color: #67c23a;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.active-label {
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
color: var(--edu-green-primary, #00875a);
|
||
}
|
||
|
||
.detail-count {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.detail-pagination {
|
||
margin-top: 12px;
|
||
text-align: right;
|
||
}
|
||
}
|
||
</style>
|