forked from liweijie/education
1、添加实施计划统计分析;
2、添加单课程实施计划列表; 3、恢复同步班历; 4、冲突项添加颜色分级; 5、校历补上正课; 6、添加教学场地总览;
This commit is contained in:
@@ -108,3 +108,85 @@ export function cancelScheduleAdjust(params) {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 运行分析 /course-running/analysis ====================
|
||||
// 接口依据:CourseRunningController 五个 analysis 端点(POST,body 为 OperationAnalysisQueryDTO)
|
||||
|
||||
/**
|
||||
* 教学运行概览指标
|
||||
* POST /course-running/analysis/overview
|
||||
* 请求体:{ nd, xqd, jxxbh, startDate, endDate, dimension }
|
||||
* 返回:OperationOverviewVO(排课完成率/缺勤率/场地使用率/周均课时/调课数等)
|
||||
*/
|
||||
export function getAnalysisOverview(data) {
|
||||
return request({
|
||||
url: '/course-running/analysis/overview',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 各教学系排课完成率
|
||||
* POST /course-running/analysis/course-completion
|
||||
* 返回:List<CourseCompletionByDeptVO>(jxxbh/jxxmc/completionRate/completedCount/totalCount)
|
||||
*/
|
||||
export function getCourseCompletion(data) {
|
||||
return request({
|
||||
url: '/course-running/analysis/course-completion',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 场地使用率分布
|
||||
* POST /course-running/analysis/venue-usage
|
||||
* 返回:List<VenueUsageRateVO>(cdlybh/cdlymc/usageRate/usageCount/totalCount)
|
||||
*/
|
||||
export function getVenueUsage(data) {
|
||||
return request({
|
||||
url: '/course-running/analysis/venue-usage',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 调课频次趋势
|
||||
* POST /course-running/analysis/adjust-trend
|
||||
* 返回:List<ScheduleAdjustTrendVO>(period/periodName/adjustCount)
|
||||
*/
|
||||
export function getAdjustTrend(data) {
|
||||
return request({
|
||||
url: '/course-running/analysis/adjust-trend',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出运行分析报表(四个 sheet:概览/系别完成率/场地使用率/调课趋势)
|
||||
* POST /course-running/analysis/export
|
||||
*/
|
||||
export function exportAnalysisReport(data) {
|
||||
return request({
|
||||
url: '/course-running/analysis/export',
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出教学实施计划课次列表
|
||||
* GET /course-running/plan/export(参数同 listTeachingPlan,无分页)
|
||||
*/
|
||||
export function exportTeachingPlan(params) {
|
||||
return request({
|
||||
url: '/course-running/plan/export',
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div class="app-container analysis-page">
|
||||
<!-- 筛选区 -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<div class="section-title">实施计划分析</div>
|
||||
<div class="section-desc">教学运行关键指标与分布统计,数据来自课程教学运行模块</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-form inline class="toolbar-form" @submit.native.prevent>
|
||||
<el-form-item label="年度">
|
||||
<el-select v-model="query.nd" placeholder="全部" clearable filterable style="width: 120px">
|
||||
<el-option v-for="y in yearOptions" :key="y" :label="y + ' 年'" :value="y" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="学期第次">
|
||||
<el-select v-model="query.xqd" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option v-for="n in [1, 2, 3, 4]" :key="n" :label="'第 ' + n + ' 学期'" :value="String(n)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="handleReset">重置</el-button>
|
||||
<el-button type="primary" size="mini" icon="el-icon-search" :loading="loading" @click="loadAll">查询</el-button>
|
||||
<el-button type="success" size="mini" icon="el-icon-download" :loading="exporting" @click="handleExport">导出报表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 概览指标卡 -->
|
||||
<div class="metric-grid" v-loading="loading">
|
||||
<div v-for="m in metricCards" :key="m.label" class="metric-card">
|
||||
<div class="metric-value" :style="{ color: m.color }">{{ m.value }}</div>
|
||||
<div class="metric-label">{{ m.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 明细统计 -->
|
||||
<div class="detail-grid">
|
||||
<div class="section-card">
|
||||
<div class="section-title small">各教学系排课完成率</div>
|
||||
<el-table :data="completionList" size="mini" border stripe v-loading="loading">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column prop="jxxmc" label="教学系" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="完成 / 总数" width="110" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.completedCount || 0 }} / {{ scope.row.totalCount || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成率" min-width="160" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-progress :percentage="pct(scope.row.completionRate)" :stroke-width="12" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && completionList.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-title small">场地使用率分布</div>
|
||||
<el-table :data="venueList" size="mini" border stripe v-loading="loading">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column prop="cdlymc" label="场地类型" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="使用 / 总数" width="110" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.usageCount || 0 }} / {{ scope.row.totalCount || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用率" min-width="160" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-progress :percentage="pct(scope.row.usageRate)" :stroke-width="12" color="#e6a23c" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && venueList.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-title small">学期调课频次趋势</div>
|
||||
<el-table :data="trendList" size="mini" border stripe v-loading="loading">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column prop="periodName" label="时段" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="adjustCount" label="调课次数" width="120" align="center" />
|
||||
<el-table-column label="分布" min-width="200" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-progress
|
||||
:percentage="trendPct(scope.row.adjustCount)"
|
||||
:stroke-width="12"
|
||||
:show-text="false"
|
||||
color="#f56c6c"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && trendList.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getAnalysisOverview,
|
||||
getCourseCompletion,
|
||||
getVenueUsage,
|
||||
getAdjustTrend,
|
||||
exportAnalysisReport
|
||||
} from '@/api/teachBusiness/courseRunning'
|
||||
import { listAllSemester } from '@/api/teachBusiness/semester'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
export default {
|
||||
name: 'OperationAnalysis',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
exporting: false,
|
||||
query: { nd: undefined, xqd: undefined, jxxbh: undefined },
|
||||
dateRange: null,
|
||||
yearOptions: [],
|
||||
overview: {},
|
||||
completionList: [],
|
||||
venueList: [],
|
||||
trendList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
metricCards() {
|
||||
const o = this.overview || {}
|
||||
const pct = v => (v === null || v === undefined) ? '-' : Number(v).toFixed(1) + '%'
|
||||
const num = v => (v === null || v === undefined) ? '-' : v
|
||||
return [
|
||||
{ label: '整体排课完成率', value: pct(o.overallCourseCompletionRate), color: '#00875a' },
|
||||
{ label: '学员缺勤率', value: pct(o.studentAbsenceRate), color: '#e6a23c' },
|
||||
{ label: '场地平均使用率', value: pct(o.venueUsageRate), color: '#e6a23c' },
|
||||
{ label: '教师周均课时', value: num(o.avgWeeklyHoursPerTeacher), color: '#303133' },
|
||||
{ label: '调课申请总数', value: num(o.totalScheduleAdjustCount), color: '#303133' },
|
||||
{ label: '待审批调课', value: num(o.pendingAdjustCount), color: '#f56c6c' },
|
||||
{ label: '保障计划数', value: num(o.totalSupportPlanCount), color: '#303133' },
|
||||
{ label: '请假总数 / 待审批', value: num(o.totalLeaveCount) + ' / ' + num(o.pendingLeaveCount), color: '#303133' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadYears()
|
||||
this.loadAll()
|
||||
},
|
||||
methods: {
|
||||
pct(v) {
|
||||
const n = Number(v)
|
||||
if (isNaN(n)) return 0
|
||||
return Math.min(100, Math.max(0, Math.round(n * 10) / 10))
|
||||
},
|
||||
trendPct(count) {
|
||||
const max = Math.max(1, ...this.trendList.map(t => t.adjustCount || 0))
|
||||
return Math.round(((count || 0) / max) * 100)
|
||||
},
|
||||
buildQuery() {
|
||||
const q = {
|
||||
nd: this.query.nd ? String(this.query.nd) : undefined,
|
||||
xqd: this.query.xqd || undefined
|
||||
}
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
q.startDate = this.dateRange[0] + 'T00:00:00'
|
||||
q.endDate = this.dateRange[1] + 'T23:59:59'
|
||||
}
|
||||
return q
|
||||
},
|
||||
loadYears() {
|
||||
listAllSemester().then(response => {
|
||||
const set = new Set()
|
||||
;(response.data || []).forEach(item => {
|
||||
if (item.nd !== null && item.nd !== undefined && item.nd !== '') set.add(item.nd)
|
||||
})
|
||||
this.yearOptions = Array.from(set).sort((a, b) => String(b).localeCompare(String(a)))
|
||||
if (!this.query.nd && this.yearOptions.length) {
|
||||
const current = (response.data || []).find(i => i.dqxq === true)
|
||||
this.query.nd = String((current && current.nd) || this.yearOptions[0])
|
||||
this.loadAll()
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
loadAll() {
|
||||
const q = this.buildQuery()
|
||||
this.loading = true
|
||||
Promise.all([
|
||||
getAnalysisOverview(q),
|
||||
getCourseCompletion(q),
|
||||
getVenueUsage(q),
|
||||
getAdjustTrend(q)
|
||||
]).then(([ov, cc, vu, at]) => {
|
||||
this.overview = ov.data || {}
|
||||
this.completionList = cc.data || []
|
||||
this.venueList = vu.data || []
|
||||
this.trendList = at.data || []
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleReset() {
|
||||
this.query = { nd: undefined, xqd: undefined, jxxbh: undefined }
|
||||
this.dateRange = null
|
||||
this.loadAll()
|
||||
},
|
||||
handleExport() {
|
||||
this.exporting = true
|
||||
exportAnalysisReport(this.buildQuery()).then(blob => {
|
||||
saveAs(blob, '运行分析报表.xlsx')
|
||||
}).catch(() => {
|
||||
this.$message.error('导出失败')
|
||||
}).finally(() => {
|
||||
this.exporting = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.analysis-page {
|
||||
.section-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.toolbar-form ::v-deep .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
|
||||
&.small {
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
padding: 18px 20px;
|
||||
text-align: center;
|
||||
|
||||
.metric-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1578,8 +1578,8 @@ export default {
|
||||
}
|
||||
|
||||
&.is-warn {
|
||||
background: #fdf8e8;
|
||||
box-shadow: inset 0 0 0 1px #e6c974;
|
||||
background: #fdf6ec;
|
||||
box-shadow: inset 0 0 0 1px #e6a23c;
|
||||
}
|
||||
|
||||
&.is-picked {
|
||||
@@ -1708,14 +1708,14 @@ export default {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 冲突格着色(橙色,按所选冲突维度命中) */
|
||||
/* 冲突格着色(深红=硬冲突,与冲突明细页色板统一;橙色仅用于软提示) */
|
||||
.tb-slot.is-conflict {
|
||||
background: #fdece0;
|
||||
box-shadow: inset 0 0 0 2px #d97706;
|
||||
background: #fde2e2;
|
||||
box-shadow: inset 0 0 0 2px #f56c6c;
|
||||
}
|
||||
|
||||
.dot-cur { background: #f7c9d8; border: 1px solid #c2477a; }
|
||||
.dot-conflict { background: #fdece0; border: 1px solid #d97706; }
|
||||
.dot-conflict { background: #fde2e2; border: 1px solid #f56c6c; }
|
||||
|
||||
/* 全学期视图:每周一段,可跨周框选 */
|
||||
.timetable.is-overview .tb-weekline {
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="app-container plan-page">
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<div class="section-title">教学实施计划</div>
|
||||
<div class="section-desc">按日期 / 节次排序的课次列表,含主讲、辅讲与上课场地</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="handleReset">重置</el-button>
|
||||
<el-button type="primary" size="mini" icon="el-icon-search" :loading="loading" @click="handleQuery">查询</el-button>
|
||||
<el-button type="success" size="mini" icon="el-icon-download" :loading="exporting" @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-form inline class="filter-form" @submit.native.prevent>
|
||||
<el-form-item label="年度">
|
||||
<el-select v-model="query.nd" placeholder="全部" clearable filterable style="width: 120px" @change="handleQuery">
|
||||
<el-option v-for="y in yearOptions" :key="y" :label="y + ' 年'" :value="Number(y)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="上课日期">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width: 260px"
|
||||
@change="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="教学方法">
|
||||
<el-input v-model="query.jxff" placeholder="如:理论讲授" clearable style="width: 140px" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="填写状态">
|
||||
<el-select v-model="query.txzt" placeholder="全部" clearable style="width: 120px" @change="handleQuery">
|
||||
<el-option label="未填写" :value="0" />
|
||||
<el-option label="已填写" :value="1" />
|
||||
<el-option label="已提交" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<el-table v-loading="loading" :data="sortedRecords" border stripe size="mini">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column prop="kcmc" label="课程名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="kcxh" label="课次" width="70" align="center" />
|
||||
<el-table-column label="上课日期" width="110" 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="zjyxm" label="主讲" min-width="90" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="fjyxm" label="辅讲" min-width="110" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ scope.row.fjyxm || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="jsmc" label="场地" min-width="120" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ scope.row.jsmc || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="jxnr" label="教学内容" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="jxff" label="教学方法" width="100" align="center" />
|
||||
<el-table-column label="填写状态" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="txztTag(scope.row.txzt)">{{ txztText(scope.row.txzt) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
class="detail-pagination"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:current-page="query.pageNum"
|
||||
:page-size="query.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
<el-empty v-if="total === 0 && !loading" description="暂无课次数据" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listTeachingPlan, exportTeachingPlan } from '@/api/teachBusiness/courseRunning'
|
||||
import { listAllSemester } from '@/api/teachBusiness/semester'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
export default {
|
||||
name: 'TeachingPlan',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
exporting: false,
|
||||
query: { nd: undefined, jxff: '', txzt: undefined, pageNum: 1, pageSize: 20 },
|
||||
dateRange: null,
|
||||
yearOptions: [],
|
||||
records: [],
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/* 按上课日期 + 节次排序(后端按课程分组排序,本页按时间轴展示) */
|
||||
sortedRecords() {
|
||||
return [...this.records].sort((a, b) => {
|
||||
const da = this.fmtDate(a.rq)
|
||||
const db = this.fmtDate(b.rq)
|
||||
if (da !== db) return da < db ? -1 : 1
|
||||
return (a.jc || 0) - (b.jc || 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadYears()
|
||||
this.loadList()
|
||||
},
|
||||
methods: {
|
||||
fmtDate(rq) {
|
||||
return (rq || '').substring(0, 10)
|
||||
},
|
||||
txztText(v) {
|
||||
return v === 2 ? '已提交' : v === 1 ? '已填写' : '未填写'
|
||||
},
|
||||
txztTag(v) {
|
||||
return v === 2 ? 'success' : v === 1 ? 'primary' : 'info'
|
||||
},
|
||||
buildParams() {
|
||||
const p = {
|
||||
nd: this.query.nd,
|
||||
jxff: this.query.jxff || undefined,
|
||||
txzt: this.query.txzt,
|
||||
pageNum: this.query.pageNum,
|
||||
pageSize: this.query.pageSize
|
||||
}
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
p.rqStart = this.dateRange[0] + 'T00:00:00'
|
||||
p.rqEnd = this.dateRange[1] + 'T23:59:59'
|
||||
}
|
||||
return p
|
||||
},
|
||||
loadYears() {
|
||||
listAllSemester().then(response => {
|
||||
const set = new Set()
|
||||
;(response.data || []).forEach(item => {
|
||||
if (item.nd !== null && item.nd !== undefined && item.nd !== '') set.add(item.nd)
|
||||
})
|
||||
this.yearOptions = Array.from(set).sort((a, b) => String(b).localeCompare(String(a)))
|
||||
if (!this.query.nd && this.yearOptions.length) {
|
||||
const current = (response.data || []).find(i => i.dqxq === true)
|
||||
this.query.nd = Number((current && current.nd) || this.yearOptions[0])
|
||||
this.loadList()
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
loadList() {
|
||||
this.loading = true
|
||||
listTeachingPlan(this.buildParams()).then(response => {
|
||||
const page = response.data || {}
|
||||
this.records = page.records || []
|
||||
this.total = page.total || 0
|
||||
}).catch(() => {
|
||||
this.records = []
|
||||
this.total = 0
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleQuery() {
|
||||
this.query.pageNum = 1
|
||||
this.loadList()
|
||||
},
|
||||
handleReset() {
|
||||
this.query = { nd: undefined, jxff: '', txzt: undefined, pageNum: 1, pageSize: 20 }
|
||||
this.dateRange = null
|
||||
this.loadList()
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.query.pageNum = page
|
||||
this.loadList()
|
||||
},
|
||||
handleSizeChange(size) {
|
||||
this.query.pageSize = size
|
||||
this.query.pageNum = 1
|
||||
this.loadList()
|
||||
},
|
||||
handleExport() {
|
||||
const p = this.buildParams()
|
||||
delete p.pageNum
|
||||
delete p.pageSize
|
||||
this.exporting = true
|
||||
exportTeachingPlan(p).then(blob => {
|
||||
saveAs(blob, '教学实施计划.xlsx')
|
||||
}).catch(() => {
|
||||
this.$message.error('导出失败')
|
||||
}).finally(() => {
|
||||
this.exporting = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.plan-page {
|
||||
.section-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.filter-form ::v-deep .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-pagination {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -61,8 +61,27 @@
|
||||
<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">
|
||||
<div class="level-legend">
|
||||
<span class="legend-item"><i class="legend-swatch legend-hard" />硬冲突(双占 / 历表不可排)</span>
|
||||
<span class="legend-item"><i class="legend-swatch legend-soft" />提示(非正课时段排课)</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="detailLoading"
|
||||
:data="activeDetails"
|
||||
border
|
||||
stripe
|
||||
size="mini"
|
||||
max-height="500"
|
||||
:row-class-name="conflictRowClass"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column label="级别" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.level === 'soft' ? 'warning' : 'danger'" size="mini" effect="dark">
|
||||
{{ scope.row.level === 'soft' ? '提示' : '硬冲突' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="冲突号" width="90" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ scope.row.conflictNo || '-' }}</template>
|
||||
</el-table-column>
|
||||
@@ -75,6 +94,7 @@
|
||||
<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-column prop="reason" label="冲突说明" min-width="200" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="detailTotal > 0"
|
||||
@@ -157,6 +177,10 @@ export default {
|
||||
fmtDate(rq) {
|
||||
return (rq || '').substring(0, 10)
|
||||
},
|
||||
/* 硬冲突行深红底、提示行橙底(与排课窗软硬两级口径一致) */
|
||||
conflictRowClass({ row }) {
|
||||
return row && row.level === 'soft' ? 'conflict-row-soft' : 'conflict-row-hard'
|
||||
},
|
||||
getStatus(item) {
|
||||
if (!item.checked) return { type: 'info', text: '未检查' }
|
||||
return item.count > 0
|
||||
@@ -416,6 +440,45 @@ export default {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.level-legend {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.legend-swatch {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend-hard {
|
||||
background: #fde2e2;
|
||||
border: 1px solid #f56c6c;
|
||||
}
|
||||
|
||||
.legend-soft {
|
||||
background: #fdf6ec;
|
||||
border: 1px solid #e6a23c;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-table .conflict-row-hard td {
|
||||
background-color: #fde2e2;
|
||||
}
|
||||
|
||||
::v-deep .el-table .conflict-row-soft td {
|
||||
background-color: #fdf6ec;
|
||||
}
|
||||
|
||||
.detail-pagination {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<div class="list-header">
|
||||
<div class="list-title">教室列表</div>
|
||||
<div class="list-actions">
|
||||
<el-button icon="el-icon-view" @click="openScheduleOverview">场地占用总览</el-button>
|
||||
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,6 +207,47 @@
|
||||
|
||||
<!-- 场地历编辑弹窗 -->
|
||||
<VenueCalendarEditor :visible.sync="calendarVisible" :venue="calendarVenue" />
|
||||
|
||||
<!-- 场地占用总览:某日某节全部场地的占用状态 -->
|
||||
<el-dialog title="场地占用总览" :visible.sync="scheduleVisible" width="860px" append-to-body>
|
||||
<el-form inline @submit.native.prevent>
|
||||
<el-form-item label="日期">
|
||||
<el-date-picker
|
||||
v-model="scheduleQuery.rq"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="选择日期"
|
||||
style="width: 160px"
|
||||
@change="loadScheduleOverview"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="节次">
|
||||
<el-select v-model="scheduleQuery.jc" placeholder="选择节次" style="width: 120px" @change="loadScheduleOverview">
|
||||
<el-option v-for="n in 12" :key="n" :label="'第 ' + n + ' 节'" :value="n" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="scheduleLoading" :data="scheduleList" border stripe size="mini" max-height="460">
|
||||
<el-table-column prop="jsbh" label="教室编号" width="110" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="jsmc" label="教室名称" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="jxldh" label="教学楼" width="110" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="scheduleStatusTag(scope.row.status)">{{ scheduleStatusText(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="kcmc" label="占用课程" min-width="140" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ scope.row.kcmc || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="xydmc" label="学员队" min-width="110" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ scope.row.xydmc || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="mc" label="备注" min-width="120" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ scope.row.mc || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!scheduleLoading && scheduleList.length === 0" description="请选择日期与节次查看占用情况" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -218,6 +260,7 @@ import {
|
||||
listAllClassroom
|
||||
} from '@/api/teachBusiness/classroom'
|
||||
import { listAllBuild } from '@/api/teachBusiness/build'
|
||||
import { venueSchedule } from '@/api/teachBusiness/venueCalendar'
|
||||
import VenueCalendarEditor from './calendarEditor.vue'
|
||||
|
||||
export default {
|
||||
@@ -250,6 +293,11 @@ export default {
|
||||
// 场地历编辑弹窗
|
||||
calendarVisible: false,
|
||||
calendarVenue: null,
|
||||
// 场地占用总览
|
||||
scheduleVisible: false,
|
||||
scheduleLoading: false,
|
||||
scheduleQuery: { rq: '', jc: undefined },
|
||||
scheduleList: [],
|
||||
rules: {
|
||||
jsmc: [{ required: true, message: '请输入教室名称', trigger: 'blur' }]
|
||||
}
|
||||
@@ -277,6 +325,37 @@ export default {
|
||||
const base = [item.jxldh, item.jxlmc].filter(Boolean).join(' ')
|
||||
return base || item.id
|
||||
},
|
||||
|
||||
/* ---------- 场地占用总览 ---------- */
|
||||
openScheduleOverview() {
|
||||
this.scheduleVisible = true
|
||||
if (!this.scheduleQuery.rq) {
|
||||
const now = new Date()
|
||||
const p = n => String(n).padStart(2, '0')
|
||||
this.scheduleQuery.rq = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`
|
||||
}
|
||||
this.loadScheduleOverview()
|
||||
},
|
||||
loadScheduleOverview() {
|
||||
if (!this.scheduleQuery.rq || !this.scheduleQuery.jc) {
|
||||
this.scheduleList = []
|
||||
return
|
||||
}
|
||||
this.scheduleLoading = true
|
||||
venueSchedule(this.scheduleQuery.rq + ' 00:00:00', this.scheduleQuery.jc).then(response => {
|
||||
this.scheduleList = response.data || []
|
||||
}).catch(() => {
|
||||
this.scheduleList = []
|
||||
}).finally(() => {
|
||||
this.scheduleLoading = false
|
||||
})
|
||||
},
|
||||
scheduleStatusText(status) {
|
||||
return { free: '空闲', unavailable: '不可用', occupied: '已占用', noted: '有备注' }[status] || status || '-'
|
||||
},
|
||||
scheduleStatusTag(status) {
|
||||
return { free: 'success', unavailable: 'danger', occupied: 'warning', noted: 'info' }[status] || 'info'
|
||||
},
|
||||
/**
|
||||
* 独立加载全部场地类型,避免查询结果收窄后下拉候选项同步丢失。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user