1、添加实施计划统计分析;
2、添加单课程实施计划列表; 3、恢复同步班历; 4、冲突项添加颜色分级; 5、校历补上正课; 6、添加教学场地总览;
This commit is contained in:
+2
-1
@@ -13,10 +13,11 @@
|
||||
<result column="备注显示" property="bzxs" />
|
||||
<result column="自动排课" property="zdpk" />
|
||||
<result column="节次" property="courseClass"/>
|
||||
<result column="正课" property="zk"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
编号, 年度, 假期时间, 假期名称, 加粗, 备注, 可排课, 备注显示, 自动排课, 节次
|
||||
编号, 年度, 假期时间, 假期名称, 加粗, 备注, 可排课, 备注显示, 自动排课, 节次, 正课
|
||||
</sql>
|
||||
|
||||
<!-- 分页查询 -->
|
||||
|
||||
+20
-37
@@ -59,6 +59,12 @@ public class ClassSemesterServiceImpl implements ClassSemesterService {
|
||||
@Resource
|
||||
private SemesterMapper semesterMapper;
|
||||
|
||||
@Resource
|
||||
private com.roomroot.jwgl.mapper.JQBMapper jqbMapper;
|
||||
|
||||
@Resource
|
||||
private com.roomroot.jwgl.service.ClassEventCalendarService classEventCalendarService;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void add(XYDNDXQJBXXB xydndxqjbxxb) {
|
||||
@@ -247,46 +253,23 @@ public class ClassSemesterServiceImpl implements ClassSemesterService {
|
||||
}
|
||||
|
||||
// 更新日期范围
|
||||
xydndxqjbxxb.setKxrq(LocalDate.parse(xqkssj));
|
||||
xydndxqjbxxb.setJsrq(LocalDate.parse(xqjssj));
|
||||
LocalDate kxrq = LocalDate.parse(xqkssj);
|
||||
LocalDate jsrq = LocalDate.parse(xqjssj);
|
||||
xydndxqjbxxb.setKxrq(kxrq);
|
||||
xydndxqjbxxb.setJsrq(jsrq);
|
||||
classSemesterMapper.updateById(xydndxqjbxxb);
|
||||
count++;
|
||||
|
||||
// // 删除日期范围之外的日历记录
|
||||
// classSemesterCalendarMapper.deleteOutsideDateRange(bh, xqkssj, xqjssj);
|
||||
//
|
||||
// // 查询现有任务记录
|
||||
// List<XYDRWB> existingCalendar = classSemesterCalendarMapper.selectByBcxqbhAndDateRange(bh, xqkssj, xqjssj);
|
||||
//
|
||||
// // 查询学期校历,用于补充缺少的记录
|
||||
// String kxrqStr = xydndxqjbxxb.getKxrq() != null ? xydndxqjbxxb.getKxrq().toString() : xqkssj;
|
||||
// String jsrqStr = xydndxqjbxxb.getJsrq() != null ? xydndxqjbxxb.getJsrq().toString() : xqjssj;
|
||||
// List<XQXLB> xqxlbs = semesterCalendarMapper.selectByNdAndJqsjRange(xydndxqjbxxb.getNd(), kxrqStr, jsrqStr);
|
||||
//
|
||||
// if (!CollectionUtils.isEmpty(xqxlbs)) {
|
||||
// // 提取已有编号
|
||||
// java.util.Set<String> existingBhs = new java.util.HashSet<>();
|
||||
// for (XYDRWB cal : existingCalendar) {
|
||||
// existingBhs.add(cal.getBh());
|
||||
// }
|
||||
//
|
||||
// // 补充缺少的记录
|
||||
// for (XQXLB xqxlb : xqxlbs) {
|
||||
// XYDRWB xydrwb = new XYDRWB();
|
||||
// String newBh = UuidUtil.getUUID();
|
||||
// if (!existingBhs.contains(newBh)) {
|
||||
// xydrwb.setBh(newBh);
|
||||
// xydrwb.setNd(xydndxqjbxxb.getNd());
|
||||
// xydrwb.setXydbh(xydndxqjbxxb.getXydbh());
|
||||
// xydrwb.setXqdc(xydndxqjbxxb.getXqdc());
|
||||
// xydrwb.setXydxqbh(bh);
|
||||
// xydrwb.setDelFlag(0);
|
||||
// classSemesterCalendarMapper.insert(xydrwb);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// count++;
|
||||
// 同步班历(假期表 JQB):删除新范围之外的格子,再按校历补齐范围内缺失的格子。
|
||||
// JQB.假期时间 存的是 yyyyMMdd 整数(与班历写入口径一致)。
|
||||
int start = Integer.parseInt(kxrq.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
|
||||
int end = Integer.parseInt(jsrq.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
|
||||
jqbMapper.delete(new LambdaQueryWrapper<com.roomroot.jwgl.entity.JQB>()
|
||||
.eq(com.roomroot.jwgl.entity.JQB::getXydxqbh, bh)
|
||||
.and(w -> w.lt(com.roomroot.jwgl.entity.JQB::getJqsj, start)
|
||||
.or().gt(com.roomroot.jwgl.entity.JQB::getJqsj, end)));
|
||||
// 只补缺失格、不覆盖人工改动(与班历弹窗「同步校历」同一口径)
|
||||
classEventCalendarService.resync(bh, true);
|
||||
}
|
||||
|
||||
return count;
|
||||
|
||||
+76
-1
@@ -29,8 +29,12 @@ import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 教学运行数据导入服务实现类。
|
||||
@@ -190,6 +194,9 @@ public class CourseRunningImportServiceImpl implements CourseRunningImportServic
|
||||
@Resource
|
||||
private OperationAnalysisMapper operationAnalysisMapper;
|
||||
|
||||
@Resource
|
||||
private com.roomroot.jwgl.mapper.ClassRoomMapper classRoomMapper;
|
||||
|
||||
/**
|
||||
* 教员表数据访问接口,用于审批时根据教员编号查询教研室代号。
|
||||
*/
|
||||
@@ -569,7 +576,10 @@ public class CourseRunningImportServiceImpl implements CourseRunningImportServic
|
||||
query.getTxzt()
|
||||
);
|
||||
|
||||
// 步骤5:封装分页结果并返回
|
||||
// 步骤5:回填主讲/辅讲教员与上课场地(按本页课次批量查,避免逐行查库)
|
||||
fillLessonTeachersAndRooms(resultPage.getRecords());
|
||||
|
||||
// 步骤6:封装分页结果并返回
|
||||
return new PageResult<>(
|
||||
resultPage.getRecords(),
|
||||
resultPage.getTotal(),
|
||||
@@ -578,6 +588,71 @@ public class CourseRunningImportServiceImpl implements CourseRunningImportServic
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填课次的主讲/辅讲教员姓名与场地名称。
|
||||
* <p>主讲 = 实施_课程表_辅助教员.主教员=1;其余为辅讲。场地取 实施_课程表_教室 关联的教室名称。</p>
|
||||
*/
|
||||
private void fillLessonTeachersAndRooms(List<TeachingPlanLessonVO> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> lessonBhs = records.stream()
|
||||
.map(TeachingPlanLessonVO::getSskcbbh)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (lessonBhs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, List<String>> mainTeachers = new HashMap<>();
|
||||
Map<String, List<String>> subTeachers = new HashMap<>();
|
||||
Set<String> jybhSet = new HashSet<>();
|
||||
for (SSKCBFZJY link : sskcbfzjymMapper.selectList(new LambdaQueryWrapper<SSKCBFZJY>()
|
||||
.in(SSKCBFZJY::getSskcbbh, lessonBhs))) {
|
||||
if (link.getFzjybh() == null || link.getFzjybh().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
jybhSet.add(link.getFzjybh());
|
||||
(link.getZjy() != null && link.getZjy() == 1 ? mainTeachers : subTeachers)
|
||||
.computeIfAbsent(link.getSskcbbh(), k -> new ArrayList<>())
|
||||
.add(link.getFzjybh());
|
||||
}
|
||||
Map<String, String> teacherNames = jybhSet.isEmpty() ? new HashMap<>()
|
||||
: jybMapper.selectList(new LambdaQueryWrapper<com.roomroot.jwgl.entity.JYB>()
|
||||
.in(com.roomroot.jwgl.entity.JYB::getJybh, jybhSet))
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(com.roomroot.jwgl.entity.JYB::getJybh,
|
||||
j -> j.getJyxm() == null ? j.getJybh() : j.getJyxm(), (a, b) -> a));
|
||||
Map<String, List<String>> rooms = new HashMap<>();
|
||||
Set<String> jsbhSet = new HashSet<>();
|
||||
for (SSKCBJS link : sskcbjsMapper.selectList(new LambdaQueryWrapper<SSKCBJS>()
|
||||
.in(SSKCBJS::getSskcbbh, lessonBhs))) {
|
||||
if (link.getJsbh() == null || link.getJsbh().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
jsbhSet.add(link.getJsbh());
|
||||
rooms.computeIfAbsent(link.getSskcbbh(), k -> new ArrayList<>()).add(link.getJsbh());
|
||||
}
|
||||
Map<String, String> roomNames = jsbhSet.isEmpty() ? new HashMap<>()
|
||||
: classRoomMapper.selectList(new LambdaQueryWrapper<com.roomroot.jwgl.entity.JSB>()
|
||||
.in(com.roomroot.jwgl.entity.JSB::getJsbh, jsbhSet)
|
||||
.eq(com.roomroot.jwgl.entity.JSB::getDelFlag, 0))
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(com.roomroot.jwgl.entity.JSB::getJsbh,
|
||||
j -> j.getJsmc() == null ? j.getJsbh() : j.getJsmc(), (a, b) -> a));
|
||||
for (TeachingPlanLessonVO vo : records) {
|
||||
String bh = vo.getSskcbbh();
|
||||
vo.setZjyxm(mainTeachers.getOrDefault(bh, List.of()).stream()
|
||||
.map(b2 -> teacherNames.getOrDefault(b2, b2)).collect(Collectors.joining("、")));
|
||||
vo.setFjyxm(subTeachers.getOrDefault(bh, List.of()).stream()
|
||||
.map(b2 -> teacherNames.getOrDefault(b2, b2)).collect(Collectors.joining("、")));
|
||||
vo.setJsmc(rooms.getOrDefault(bh, List.of()).stream()
|
||||
.map(b2 -> roomNames.getOrDefault(b2, b2)).collect(Collectors.joining("、")));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按查询条件导出课次数据为 Excel 文件。
|
||||
*
|
||||
|
||||
+73
-2
@@ -280,9 +280,14 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
|
||||
case EVENT_CONFLICT -> checkEventConflicts(nd);
|
||||
case GUARANTEE_CONFLICT -> Collections.emptyList();
|
||||
};
|
||||
// 本检查器的维度全部为硬冲突(双占 / 三类历不可排);软提示由排课窗实时计算
|
||||
// 未显式标注级别的明细默认硬冲突(双占 / 三类历不可排);
|
||||
// checkEventConflicts 中"非正课时段"已标 soft,不在此覆盖
|
||||
if (details != null) {
|
||||
details.forEach(d -> d.setLevel("hard"));
|
||||
details.forEach(d -> {
|
||||
if (d.getLevel() == null || d.getLevel().isEmpty()) {
|
||||
d.setLevel("hard");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[课表冲突检查][维度:{}][年度:{}] 执行SQL失败: {}",
|
||||
@@ -589,6 +594,72 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 软提示:非正课时段(正课=0 但可排课)已有课次 → 橙色提示,不算硬冲突 ----
|
||||
Set<String> hardSchoolKeys = new HashSet<>();
|
||||
for (XQXLB grid : schoolUnavailable) {
|
||||
for (Integer jc : PeriodUtil.parsePeriods(grid.getCourseClass())) {
|
||||
hardSchoolKeys.add(grid.getJqsj() + "#" + jc);
|
||||
}
|
||||
}
|
||||
List<XQXLB> schoolSoft = semesterCalendarMapper.selectList(new LambdaQueryWrapper<XQXLB>()
|
||||
.eq(XQXLB::getNd, nd)
|
||||
.eq(XQXLB::getZk, false)
|
||||
.ne(XQXLB::getKpk, false));
|
||||
for (XQXLB grid : schoolSoft) {
|
||||
for (Integer jc : PeriodUtil.parsePeriods(grid.getCourseClass())) {
|
||||
if (hardSchoolKeys.contains(grid.getJqsj() + "#" + jc)) {
|
||||
continue;
|
||||
}
|
||||
List<SSKCB> hits = byGrid.get(grid.getJqsj() + "#" + jc);
|
||||
if (hits != null && !hits.isEmpty()) {
|
||||
TimetableConflictDetailVO d = buildEventDetail(
|
||||
"SOFT_SCHOOL|" + grid.getJqsj() + "#" + jc, "非正课时段",
|
||||
grid.getJqsj(), jc, hits, courseNames,
|
||||
"校历标记该日期节次为非正课时段,但已有课次安排");
|
||||
d.setLevel("soft");
|
||||
result.add(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
List<JQB> classSoft = semesterBhs.isEmpty() ? Collections.emptyList()
|
||||
: jqbMapper.selectList(new LambdaQueryWrapper<JQB>()
|
||||
.in(JQB::getXydxqbh, semesterBhs)
|
||||
.eq(JQB::getZk, 0)
|
||||
.ne(JQB::getKpk, 0));
|
||||
if (!classSoft.isEmpty()) {
|
||||
Map<String, SSKCB> lessonByBh = lessons.stream()
|
||||
.collect(Collectors.toMap(SSKCB::getBh, l -> l, (a, b) -> a));
|
||||
List<SSKCXYD> links = sskcxydMapper.selectList(new LambdaQueryWrapper<SSKCXYD>()
|
||||
.in(SSKCXYD::getSskcbh, lessonByBh.keySet()));
|
||||
Map<String, String> teamNames = xydbMapper.selectList(new LambdaQueryWrapper<>()).stream()
|
||||
.collect(Collectors.toMap(XYDB::getXydbh, XYDB::getXydmc, (a, b) -> a));
|
||||
Map<String, List<String>> teamsOfLesson = new HashMap<>();
|
||||
for (SSKCXYD link : links) {
|
||||
teamsOfLesson.computeIfAbsent(link.getSskcbh(), k -> new ArrayList<>()).add(link.getXydbh());
|
||||
}
|
||||
for (JQB grid : classSoft) {
|
||||
if (grid.getJqsj() == null || grid.getCourseClass() == null) continue;
|
||||
String dateStr = intDateToString(grid.getJqsj());
|
||||
for (Integer jc : PeriodUtil.parsePeriods(grid.getCourseClass())) {
|
||||
for (Map.Entry<String, List<String>> e : teamsOfLesson.entrySet()) {
|
||||
if (!e.getValue().contains(grid.getXydbh())) continue;
|
||||
SSKCB lesson = lessonByBh.get(e.getKey());
|
||||
if (lesson == null || lesson.getRq() == null || lesson.getJc() == null) continue;
|
||||
if (!dateStr.equals(lesson.getRq().toLocalDate().toString()) || lesson.getJc() != jc) continue;
|
||||
TimetableConflictDetailVO d = buildEventDetail(
|
||||
"SOFT_CLASS|" + grid.getXydbh() + "|" + dateStr + "#" + jc, "非正课时段",
|
||||
dateStr, jc, List.of(lesson), courseNames,
|
||||
"班历标记该日期节次对该学员队为非正课时段,但已有课次安排");
|
||||
d.setLevel("soft");
|
||||
d.setXydNames(teamNames.getOrDefault(grid.getXydbh(), grid.getXydbh()));
|
||||
result.add(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -59,6 +59,21 @@ public class TeachingPlanLessonVO {
|
||||
*/
|
||||
private String jxff;
|
||||
|
||||
/**
|
||||
* 主讲教员姓名(课次辅讲教员表中 主教员=1 的教员)。
|
||||
*/
|
||||
private String zjyxm;
|
||||
|
||||
/**
|
||||
* 辅讲教员姓名(多个用「、」连接)。
|
||||
*/
|
||||
private String fjyxm;
|
||||
|
||||
/**
|
||||
* 上课场地名称(课次教室关联,多个用「、」连接)。
|
||||
*/
|
||||
private String jsmc;
|
||||
|
||||
/**
|
||||
* 填写状态:0-未填写,1-已填写,2-已提交。
|
||||
*/
|
||||
|
||||
@@ -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