forked from liweijie/education
1、添加实施计划统计分析;
2、添加单课程实施计划列表; 3、恢复同步班历; 4、冲突项添加颜色分级; 5、校历补上正课; 6、添加教学场地总览;
This commit is contained in:
+2
-1
@@ -13,10 +13,11 @@
|
|||||||
<result column="备注显示" property="bzxs" />
|
<result column="备注显示" property="bzxs" />
|
||||||
<result column="自动排课" property="zdpk" />
|
<result column="自动排课" property="zdpk" />
|
||||||
<result column="节次" property="courseClass"/>
|
<result column="节次" property="courseClass"/>
|
||||||
|
<result column="正课" property="zk"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="Base_Column_List">
|
<sql id="Base_Column_List">
|
||||||
编号, 年度, 假期时间, 假期名称, 加粗, 备注, 可排课, 备注显示, 自动排课, 节次
|
编号, 年度, 假期时间, 假期名称, 加粗, 备注, 可排课, 备注显示, 自动排课, 节次, 正课
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<!-- 分页查询 -->
|
<!-- 分页查询 -->
|
||||||
|
|||||||
+20
-37
@@ -59,6 +59,12 @@ public class ClassSemesterServiceImpl implements ClassSemesterService {
|
|||||||
@Resource
|
@Resource
|
||||||
private SemesterMapper semesterMapper;
|
private SemesterMapper semesterMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.roomroot.jwgl.mapper.JQBMapper jqbMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.roomroot.jwgl.service.ClassEventCalendarService classEventCalendarService;
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
@Override
|
||||||
public void add(XYDNDXQJBXXB xydndxqjbxxb) {
|
public void add(XYDNDXQJBXXB xydndxqjbxxb) {
|
||||||
@@ -247,46 +253,23 @@ public class ClassSemesterServiceImpl implements ClassSemesterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新日期范围
|
// 更新日期范围
|
||||||
xydndxqjbxxb.setKxrq(LocalDate.parse(xqkssj));
|
LocalDate kxrq = LocalDate.parse(xqkssj);
|
||||||
xydndxqjbxxb.setJsrq(LocalDate.parse(xqjssj));
|
LocalDate jsrq = LocalDate.parse(xqjssj);
|
||||||
|
xydndxqjbxxb.setKxrq(kxrq);
|
||||||
|
xydndxqjbxxb.setJsrq(jsrq);
|
||||||
classSemesterMapper.updateById(xydndxqjbxxb);
|
classSemesterMapper.updateById(xydndxqjbxxb);
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
// // 删除日期范围之外的日历记录
|
// 同步班历(假期表 JQB):删除新范围之外的格子,再按校历补齐范围内缺失的格子。
|
||||||
// classSemesterCalendarMapper.deleteOutsideDateRange(bh, xqkssj, xqjssj);
|
// JQB.假期时间 存的是 yyyyMMdd 整数(与班历写入口径一致)。
|
||||||
//
|
int start = Integer.parseInt(kxrq.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
|
||||||
// // 查询现有任务记录
|
int end = Integer.parseInt(jsrq.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
|
||||||
// List<XYDRWB> existingCalendar = classSemesterCalendarMapper.selectByBcxqbhAndDateRange(bh, xqkssj, xqjssj);
|
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)
|
||||||
// String kxrqStr = xydndxqjbxxb.getKxrq() != null ? xydndxqjbxxb.getKxrq().toString() : xqkssj;
|
.or().gt(com.roomroot.jwgl.entity.JQB::getJqsj, end)));
|
||||||
// String jsrqStr = xydndxqjbxxb.getJsrq() != null ? xydndxqjbxxb.getJsrq().toString() : xqjssj;
|
// 只补缺失格、不覆盖人工改动(与班历弹窗「同步校历」同一口径)
|
||||||
// List<XQXLB> xqxlbs = semesterCalendarMapper.selectByNdAndJqsjRange(xydndxqjbxxb.getNd(), kxrqStr, jsrqStr);
|
classEventCalendarService.resync(bh, true);
|
||||||
//
|
|
||||||
// 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++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
|||||||
+76
-1
@@ -29,8 +29,12 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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
|
@Resource
|
||||||
private OperationAnalysisMapper operationAnalysisMapper;
|
private OperationAnalysisMapper operationAnalysisMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.roomroot.jwgl.mapper.ClassRoomMapper classRoomMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 教员表数据访问接口,用于审批时根据教员编号查询教研室代号。
|
* 教员表数据访问接口,用于审批时根据教员编号查询教研室代号。
|
||||||
*/
|
*/
|
||||||
@@ -569,7 +576,10 @@ public class CourseRunningImportServiceImpl implements CourseRunningImportServic
|
|||||||
query.getTxzt()
|
query.getTxzt()
|
||||||
);
|
);
|
||||||
|
|
||||||
// 步骤5:封装分页结果并返回
|
// 步骤5:回填主讲/辅讲教员与上课场地(按本页课次批量查,避免逐行查库)
|
||||||
|
fillLessonTeachersAndRooms(resultPage.getRecords());
|
||||||
|
|
||||||
|
// 步骤6:封装分页结果并返回
|
||||||
return new PageResult<>(
|
return new PageResult<>(
|
||||||
resultPage.getRecords(),
|
resultPage.getRecords(),
|
||||||
resultPage.getTotal(),
|
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 文件。
|
* 按查询条件导出课次数据为 Excel 文件。
|
||||||
*
|
*
|
||||||
|
|||||||
+73
-2
@@ -280,9 +280,14 @@ public class TimetableConflictServiceImpl implements TimetableConflictService {
|
|||||||
case EVENT_CONFLICT -> checkEventConflicts(nd);
|
case EVENT_CONFLICT -> checkEventConflicts(nd);
|
||||||
case GUARANTEE_CONFLICT -> Collections.emptyList();
|
case GUARANTEE_CONFLICT -> Collections.emptyList();
|
||||||
};
|
};
|
||||||
// 本检查器的维度全部为硬冲突(双占 / 三类历不可排);软提示由排课窗实时计算
|
// 未显式标注级别的明细默认硬冲突(双占 / 三类历不可排);
|
||||||
|
// checkEventConflicts 中"非正课时段"已标 soft,不在此覆盖
|
||||||
if (details != null) {
|
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) {
|
} catch (Exception e) {
|
||||||
log.error("[课表冲突检查][维度:{}][年度:{}] 执行SQL失败: {}",
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
@@ -59,6 +59,21 @@ public class TeachingPlanLessonVO {
|
|||||||
*/
|
*/
|
||||||
private String jxff;
|
private String jxff;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主讲教员姓名(课次辅讲教员表中 主教员=1 的教员)。
|
||||||
|
*/
|
||||||
|
private String zjyxm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅讲教员姓名(多个用「、」连接)。
|
||||||
|
*/
|
||||||
|
private String fjyxm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上课场地名称(课次教室关联,多个用「、」连接)。
|
||||||
|
*/
|
||||||
|
private String jsmc;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 填写状态:0-未填写,1-已填写,2-已提交。
|
* 填写状态:0-未填写,1-已填写,2-已提交。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -108,3 +108,85 @@ export function cancelScheduleAdjust(params) {
|
|||||||
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 {
|
&.is-warn {
|
||||||
background: #fdf8e8;
|
background: #fdf6ec;
|
||||||
box-shadow: inset 0 0 0 1px #e6c974;
|
box-shadow: inset 0 0 0 1px #e6a23c;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-picked {
|
&.is-picked {
|
||||||
@@ -1708,14 +1708,14 @@ export default {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 冲突格着色(橙色,按所选冲突维度命中) */
|
/* 冲突格着色(深红=硬冲突,与冲突明细页色板统一;橙色仅用于软提示) */
|
||||||
.tb-slot.is-conflict {
|
.tb-slot.is-conflict {
|
||||||
background: #fdece0;
|
background: #fde2e2;
|
||||||
box-shadow: inset 0 0 0 2px #d97706;
|
box-shadow: inset 0 0 0 2px #f56c6c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot-cur { background: #f7c9d8; border: 1px solid #c2477a; }
|
.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 {
|
.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>
|
<span v-if="activeKey" class="detail-count">共 {{ detailTotal }} 条记录</span>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="activeKey">
|
<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 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>
|
<el-table-column label="冲突号" width="90" align="center" show-overflow-tooltip>
|
||||||
<template slot-scope="scope">{{ scope.row.conflictNo || '-' }}</template>
|
<template slot-scope="scope">{{ scope.row.conflictNo || '-' }}</template>
|
||||||
</el-table-column>
|
</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="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="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="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-table>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-if="detailTotal > 0"
|
v-if="detailTotal > 0"
|
||||||
@@ -157,6 +177,10 @@ export default {
|
|||||||
fmtDate(rq) {
|
fmtDate(rq) {
|
||||||
return (rq || '').substring(0, 10)
|
return (rq || '').substring(0, 10)
|
||||||
},
|
},
|
||||||
|
/* 硬冲突行深红底、提示行橙底(与排课窗软硬两级口径一致) */
|
||||||
|
conflictRowClass({ row }) {
|
||||||
|
return row && row.level === 'soft' ? 'conflict-row-soft' : 'conflict-row-hard'
|
||||||
|
},
|
||||||
getStatus(item) {
|
getStatus(item) {
|
||||||
if (!item.checked) return { type: 'info', text: '未检查' }
|
if (!item.checked) return { type: 'info', text: '未检查' }
|
||||||
return item.count > 0
|
return item.count > 0
|
||||||
@@ -416,6 +440,45 @@ export default {
|
|||||||
color: #909399;
|
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 {
|
.detail-pagination {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="list-title">教室列表</div>
|
<div class="list-title">教室列表</div>
|
||||||
<div class="list-actions">
|
<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>
|
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,6 +207,47 @@
|
|||||||
|
|
||||||
<!-- 场地历编辑弹窗 -->
|
<!-- 场地历编辑弹窗 -->
|
||||||
<VenueCalendarEditor :visible.sync="calendarVisible" :venue="calendarVenue" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -218,6 +260,7 @@ import {
|
|||||||
listAllClassroom
|
listAllClassroom
|
||||||
} from '@/api/teachBusiness/classroom'
|
} from '@/api/teachBusiness/classroom'
|
||||||
import { listAllBuild } from '@/api/teachBusiness/build'
|
import { listAllBuild } from '@/api/teachBusiness/build'
|
||||||
|
import { venueSchedule } from '@/api/teachBusiness/venueCalendar'
|
||||||
import VenueCalendarEditor from './calendarEditor.vue'
|
import VenueCalendarEditor from './calendarEditor.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -250,6 +293,11 @@ export default {
|
|||||||
// 场地历编辑弹窗
|
// 场地历编辑弹窗
|
||||||
calendarVisible: false,
|
calendarVisible: false,
|
||||||
calendarVenue: null,
|
calendarVenue: null,
|
||||||
|
// 场地占用总览
|
||||||
|
scheduleVisible: false,
|
||||||
|
scheduleLoading: false,
|
||||||
|
scheduleQuery: { rq: '', jc: undefined },
|
||||||
|
scheduleList: [],
|
||||||
rules: {
|
rules: {
|
||||||
jsmc: [{ required: true, message: '请输入教室名称', trigger: 'blur' }]
|
jsmc: [{ required: true, message: '请输入教室名称', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
@@ -277,6 +325,37 @@ export default {
|
|||||||
const base = [item.jxldh, item.jxlmc].filter(Boolean).join(' ')
|
const base = [item.jxldh, item.jxlmc].filter(Boolean).join(' ')
|
||||||
return base || item.id
|
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