From 351df2cba2de25009c404600a74b1e67f10aa401 Mon Sep 17 00:00:00 2001 From: liweijie Date: Thu, 17 Sep 2026 17:14:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=92=E8=AF=BE=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwgl/SchedulingWindowController.java | 95 ++ .../jwgl/TeacherCalendarController.java | 12 + .../scheduling/SchedulingArrangeRequest.java | 23 + .../SchedulingCourseEditRequest.java | 31 + .../java/com/roomroot/jwgl/entity/BZTGMX.java | 14 + .../com/roomroot/jwgl/entity/KCBBZMX.java | 8 + .../com/roomroot/jwgl/mapper/SSKCMapper.java | 28 + .../com/roomroot/jwgl/mapper/SSKCMapper.xml | 77 ++ .../com/roomroot/jwgl/service/JYLService.java | 15 + .../jwgl/service/SchedulingWindowService.java | 10 + .../jwgl/service/TaskBookFillService.java | 3 +- .../jwgl/service/impl/JYLServiceImpl.java | 86 ++ .../impl/RunningCoursePublishServiceImpl.java | 28 +- .../impl/SchedulingWindowServiceImpl.java | 493 +++++++- .../impl/StudentTeamTaskServiceImpl.java | 5 +- .../service/impl/TaskBookFillServiceImpl.java | 39 +- .../impl/TimetableConflictServiceImpl.java | 121 +- .../conflict/TimetableConflictDimension.java | 23 +- .../jwgl/vo/scheduling/SchedulingViewVO.java | 49 + .../src/api/teachBusiness/schedulingWindow.js | 44 + .../src/api/teachOffice/teacherCalendar.js | 32 + .../teachBusiness/schedulingWindow/index.vue | 1091 +++++++++++++++-- .../teachBusiness/timetableConflict/index.vue | 5 +- .../views/teachOffice/shiftSemester/index.vue | 69 ++ .../components/TeacherCalendarDialog.vue | 461 +++++++ .../src/views/teachOffice/teacher/index.vue | 27 +- 26 files changed, 2699 insertions(+), 190 deletions(-) create mode 100644 backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/scheduling/SchedulingCourseEditRequest.java create mode 100644 frontend/src/api/teachOffice/teacherCalendar.js create mode 100644 frontend/src/views/teachOffice/teacher/components/TeacherCalendarDialog.vue diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/SchedulingWindowController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/SchedulingWindowController.java index d4d3231d..b7c865e2 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/SchedulingWindowController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/SchedulingWindowController.java @@ -1,7 +1,10 @@ package com.roomroot.web.controller.jwgl; +import com.roomroot.common.exception.ServiceException; import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest; import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest; +import com.roomroot.jwgl.dto.scheduling.SchedulingCourseEditRequest; +import com.roomroot.jwgl.service.RunningCoursePublishService; import com.roomroot.jwgl.service.SchedulingWindowService; import com.roomroot.jwgl.unit.Result; import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO; @@ -14,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,6 +35,9 @@ public class SchedulingWindowController { @Autowired private SchedulingWindowService schedulingWindowService; + @Autowired + private RunningCoursePublishService runningCoursePublishService; + /** * 排课窗组合视图:班次头 + 课程列表(排满标绿)+ 周次×星期×节次格子。 */ @@ -84,4 +92,91 @@ public class SchedulingWindowController { @RequestParam(value = "czlx", required = false) String czlx) { return Result.success(schedulingWindowService.logs(sskcbh, czlx)); } + + /** + * 编辑课程教学任务信息(手册 12.3.1):学时/周课时/课程简称/责任教员/默认场地。 + */ + @PreAuthorize("@ss.hasPermi('teach:schedulingWindow:edit')") + @PostMapping("/course/update") + public Result> updateCourse(@RequestBody SchedulingCourseEditRequest request) { + return Result.success(schedulingWindowService.updateCourse(request)); + } + + /** + * 教学班次(合班)当前列表 + 候选班次(手册 12.3.2.2)。 + */ + @PreAuthorize("@ss.hasPermi('teach:schedulingWindow:list')") + @GetMapping("/course/teams") + public Result> courseTeams(@RequestParam("sskcbh") String sskcbh) { + return Result.success(schedulingWindowService.courseTeams(sskcbh)); + } + + /** + * 教学保障选项:保障类别 + 保障资源(含总量与计量单位,手册 12.3.2.5)。 + */ + @PreAuthorize("@ss.hasPermi('teach:schedulingWindow:list')") + @GetMapping("/support/options") + public Result> supportOptions() { + return Result.success(schedulingWindowService.supportOptions()); + } + + /** + * 批量发布【运行课表】(手册 12.1):把班次学期的课程任务发布为运行课程。 + * + *

发布前要求教学任务已发布未结束、责任教员与默认场地齐备;逐个班次独立事务, + * 单个失败不影响其它班次,失败原因逐条返回。

+ */ + @PreAuthorize("@ss.hasPermi('teach:schedulingWindow:edit')") + @PostMapping("/publish") + public Result>> publish(@RequestBody PublishRequest request) { + return Result.success(batchPublish(request, true)); + } + + /** + * 批量删除【运行课表】(手册 12.2):撤回该班次学期的运行课程,已排课次一并删除。 + */ + @PreAuthorize("@ss.hasPermi('teach:schedulingWindow:edit')") + @PostMapping("/withdraw") + public Result>> withdraw(@RequestBody PublishRequest request) { + return Result.success(batchPublish(request, false)); + } + + /** 批量发布/撤回公共实现 */ + private List> batchPublish(PublishRequest request, boolean publish) { + if (request == null || request.getXydxqbhs() == null || request.getXydxqbhs().isEmpty()) { + throw new ServiceException("请选择要操作的班次学期", 400); + } + List> results = new ArrayList<>(); + for (String xydxqbh : request.getXydxqbhs()) { + Map item = new HashMap<>(); + item.put("xydxqbh", xydxqbh); + try { + int count = publish + ? runningCoursePublishService.publish(xydxqbh) + : runningCoursePublishService.withdraw(xydxqbh); + item.put("success", true); + item.put("count", count); + item.put("message", publish ? ("已发布 " + count + " 条运行课程") : ("已删除 " + count + " 条运行课程")); + } catch (Exception e) { + item.put("success", false); + item.put("count", 0); + item.put("message", e.getMessage() == null ? "操作失败" : e.getMessage()); + } + results.add(item); + } + return results; + } + + /** 批量发布/删除请求体 */ + public static class PublishRequest { + private List xydxqbhs; + + public List getXydxqbhs() { + return xydxqbhs; + } + + public void setXydxqbhs(List xydxqbhs) { + this.xydxqbhs = xydxqbhs; + } + } } diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TeacherCalendarController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TeacherCalendarController.java index 3c98e90a..a610487a 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TeacherCalendarController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/TeacherCalendarController.java @@ -70,4 +70,16 @@ public class TeacherCalendarController { public Result> listByRq(@RequestParam("rq") String rq) { return Result.success(jylService.listByRq(rq)); } + + /** 批量保存教员历(教员历编辑器整片提交,按 教员+日期+节次 幂等覆盖) */ + @PostMapping("/batchSave") + public Result batchSave(@RequestBody List list) { + return Result.success(jylService.batchSave(list)); + } + + /** 批量删除教员历(本表无 del_flag,物理删除) */ + @PostMapping("/batchDelete") + public Result batchDelete(@RequestBody List bhs) { + return Result.success(jylService.batchDelete(bhs)); + } } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/scheduling/SchedulingArrangeRequest.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/scheduling/SchedulingArrangeRequest.java index 09e5e0ff..5623f2cf 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/scheduling/SchedulingArrangeRequest.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/scheduling/SchedulingArrangeRequest.java @@ -42,9 +42,32 @@ public class SchedulingArrangeRequest { /** 主讲教员,缺省用课程责任教员 */ private String jybh; + /** 教学班次(学员队编号,本课程默认全部合班班次,可临时增删) */ + private List xydbhs; + + /** 辅讲教员编号(可多个) */ + private List fzjybhs; + + /** 教学场地(可多场地),为空则用 jsbh / 课程默认场地 */ + private List jsbhs; + + /** 教学保障需求(结构化:类别→资源→数量) */ + private List supports; + /** 所选格子 */ private List cells; + /** 教学保障需求项 */ + @Data + public static class Support { + /** 保障提供明细编号(保障资源库) */ + private String bztgmxbh; + /** 需求数量 */ + private Integer sl; + /** 备注 */ + private String bz; + } + @Data public static class Cell { /** 日期 yyyy-MM-dd */ diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/scheduling/SchedulingCourseEditRequest.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/scheduling/SchedulingCourseEditRequest.java new file mode 100644 index 00000000..03747ba2 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/dto/scheduling/SchedulingCourseEditRequest.java @@ -0,0 +1,31 @@ +package com.roomroot.jwgl.dto.scheduling; + +import lombok.Data; + +/** + * 排课窗口内「编辑课程教学任务信息」请求(手册 12.3.1)。 + * + *

只允许改运行编制的可调项:学时、周课时、课程简称、责任教员、默认场地。 + * 课程简称写回学员队任务表(编制侧)后运行课程同步显示。

+ */ +@Data +public class SchedulingCourseEditRequest { + + /** 实施_课程编号 */ + private String sskcbh; + + /** 学时 */ + private Integer xs; + + /** 周课时 */ + private Integer zks; + + /** 课程简称 */ + private String jc; + + /** 责任教员编号 */ + private String jybh; + + /** 默认教学场地(教室编号) */ + private String jsbh; +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/BZTGMX.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/BZTGMX.java index 95ae4988..b622bb22 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/BZTGMX.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/BZTGMX.java @@ -1,5 +1,6 @@ package com.roomroot.jwgl.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; @@ -22,66 +23,79 @@ public class BZTGMX { /** * 保障类别编号 */ + @TableField("保障类别编号") private String bzlbbh; /** * 名称 */ + @TableField("名称") private String mc; /** * 单位 */ + @TableField("单位") private String dw; /** * 数量 */ + @TableField("数量") private Integer sl; /** * 备注 */ + @TableField("备注") private String bz; /** * 停用 */ + @TableField("停用") private Integer ty; /** * 健康状态 */ + @TableField("健康状态") private Float jkzt; /** * 详细 */ + @TableField("详细") private String xx; /** * 耗材 */ + @TableField("耗材") private Integer hc; /** * 拼音 */ + @TableField("拼音") private String py; /** * 资源类别 */ + @TableField("资源类别") private String zylb; /** * JSONZD */ + @TableField("JSONZD") private String jsonzd; /** * 危险品 */ + @TableField("危险品") private Integer wxp; } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/KCBBZMX.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/KCBBZMX.java index d01c00ab..00749827 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/KCBBZMX.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/entity/KCBBZMX.java @@ -1,5 +1,6 @@ package com.roomroot.jwgl.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; @@ -24,36 +25,43 @@ public class KCBBZMX { /** * 课程表编号 */ + @TableField("课程表编号") private String kcbbh; /** * 保障提供明细编号 */ + @TableField("保障提供明细编号") private String bztgmxbh; /** * 数量 */ + @TableField("数量") private Integer sl; /** * 备注 */ + @TableField("备注") private String bz; /** * 年度 */ + @TableField("年度") private Integer nd; /** * 日期 */ + @TableField("日期") private LocalDateTime rq; /** * 节次 */ + @TableField("节次") private Integer jc; } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/SSKCMapper.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/SSKCMapper.java index 39108b94..766ab690 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/SSKCMapper.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/SSKCMapper.java @@ -12,6 +12,18 @@ public interface SSKCMapper extends BaseMapper { List selectBySemester(@Param("nd") Integer nd, @Param("xqdc") Integer xqdc); + /** + * 按年度查询全部实施_课程(不做停用过滤)。 + *

禁用 {@code selectList(wrapper)} 读本表:中文列名 + VARBINARY 主键下, + * MyBatis-Plus 自动 SQL 的列标签与 resultMap 列名不匹配,会整行返回 null 元素。

+ */ + List selectByNd(@Param("nd") Integer nd); + + /** + * 按科目编号查询实施_课程(同样绕开 selectList,原因见上)。 + */ + List selectByKmbh(@Param("kmbh") String kmbh); + List selectByClassBh(@Param("classBh") String classBh, @Param("nd") Integer nd); SSKC selectByCourseBhAndClassBh(@Param("courseBh") String courseBh, @Param("classBh") String classBh, @Param("nd") Integer nd); @@ -20,4 +32,20 @@ public interface SSKCMapper extends BaseMapper { * 按编号批量查询(编号为 VARBINARY GUID,不能用 selectBatchIds——主键 typeHandler 对 IN 参数不生效) */ List selectByBhs(@Param("list") List bhs); + + /** + * 按编号批量删除(同上:编号为 VARBINARY GUID,deleteById 直绑字符串会报 + * dm.jdbc.driver.DMException: Invalid hexadecimal digits)。 + * + * @return 删除行数 + */ + int deleteByBhs(@Param("list") List bhs); + + /** + * 按编号更新运行编制可调项(学时 / 周课时 / 责任教员 / 默认场地)。 + * 同样绕开 GUID 主键直接绑定:updateById 会对主键绑字符串而失败。 + * 传 null 的字段不更新。 + */ + int updateBasicByBh(@Param("bh") String bh, @Param("xs") Integer xs, @Param("zks") Integer zks, + @Param("jybh") String jybh, @Param("jsbh") String jsbh); } \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/SSKCMapper.xml b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/SSKCMapper.xml index 95a654d6..3eae9177 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/SSKCMapper.xml +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/mapper/SSKCMapper.xml @@ -2,9 +2,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM 实施_课程 + WHERE LOWER(RAWTOHEX("编号")) IN + + LOWER(REPLACE(TRIM(#{bh}), '-', '')) + + + + + + UPDATE 实施_课程 + + "学时" = #{xs}, + "周课时" = #{zks}, + "教员编号" = #{jybh}, + "教室编号" = #{jsbh}, + "变动时间" = CURRENT_TIMESTAMP + + WHERE LOWER(RAWTOHEX("编号")) = LOWER(REPLACE(TRIM(#{bh}), '-', '')) + + \ No newline at end of file diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/JYLService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/JYLService.java index 809f5a32..afc0ad0c 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/JYLService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/JYLService.java @@ -26,4 +26,19 @@ public interface JYLService { List listByJybh(String jybh); List listByRq(String rq); + + /** + * 批量保存教员历(供教员历编辑器整片提交)。 + *

按 (教员编号 + 日期 + 节次) 幂等覆盖:已有则更新,没有则新增。

+ * + * @return 实际写入/更新的条数 + */ + int batchSave(List list); + + /** + * 批量删除教员历(本表无 del_flag 审计列,为物理删除)。 + * + * @return 实际删除条数 + */ + int batchDelete(List bhs); } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/SchedulingWindowService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/SchedulingWindowService.java index d87f06b5..858944ba 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/SchedulingWindowService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/SchedulingWindowService.java @@ -2,6 +2,7 @@ package com.roomroot.jwgl.service; import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest; import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest; +import com.roomroot.jwgl.dto.scheduling.SchedulingCourseEditRequest; import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO; import java.util.List; @@ -36,4 +37,13 @@ public interface SchedulingWindowService { /** 排课日志查询:按课程(可叠加操作类型) */ List> logs(String sskcbh, String czlx); + + /** 编辑课程教学任务信息:学时/周课时/课程简称/责任教员/默认场地(手册 12.3.1) */ + Map updateCourse(SchedulingCourseEditRequest request); + + /** 教学班次(合班)当前列表 + 候选班次(手册 12.3.2.2) */ + Map courseTeams(String sskcbh); + + /** 教学保障选项:保障类别 + 保障资源(含总量与计量单位,手册 12.3.2.5) */ + Map supportOptions(); } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TaskBookFillService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TaskBookFillService.java index 40ff4c2b..74091ac1 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TaskBookFillService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/TaskBookFillService.java @@ -19,7 +19,8 @@ import java.util.Map; *
  • 合班分组号用已有列 学员队任务表.编组(bz2),0=未合班;不复用 预设编班号 和 课次序号。
  • *
  • 发布门禁:仅「已发布且未结束」的教学任务可填报;结束后只读(TeachingTaskWriteGuard)。
  • *
  • 责任教员 / 场地修改时,同合班组(同编组,非 0)的行同步。
  • - *
  • 已发布到运行课表的行须先撤回才能拆班(运行课表属阶段 5,当前先拦截有排课数据的行)。
  • + *
  • 已发布到运行课表的行须先撤回才能拆班:按「实施_课程学员队」(学员队编号, 年度=6 位学期代号) 判定, + * 与运行课表发布的幂等门禁同源;拆班会让任务书合班组与已固化的运行课程不一致。
  • *

    */ public interface TaskBookFillService { diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/JYLServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/JYLServiceImpl.java index c65156b0..1b2f80ee 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/JYLServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/JYLServiceImpl.java @@ -5,13 +5,19 @@ import com.roomroot.common.utils.StringUtils; import com.roomroot.jwgl.entity.JYL; import com.roomroot.jwgl.mapper.JYLMapper; import com.roomroot.jwgl.service.JYLService; +import com.roomroot.jwgl.unit.BusinessException; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; /** @@ -81,6 +87,86 @@ public class JYLServiceImpl implements JYLService { .apply("DATE(rq) = {0}", rq)); } + /** + * 批量保存:按 (教员编号 + 日期 + 节次) 幂等覆盖。 + *

    先按涉及的教员 + 日期区间一次性把已有记录拉回来判重,避免逐行查库。

    + */ + @Override + public int batchSave(List list) { + if (list == null || list.isEmpty()) { + return 0; + } + Set jybhSet = new HashSet<>(); + LocalDate minDay = null; + LocalDate maxDay = null; + for (JYL item : list) { + if (item == null || StringUtils.isEmpty(item.getJybh())) { + throw new BusinessException("教员历缺少教员编号"); + } + if (item.getRq() == null || item.getJc() == null) { + throw new BusinessException("教员历缺少日期或节次"); + } + item.setJybh(item.getJybh().trim()); + jybhSet.add(item.getJybh()); + LocalDate d = item.getRq().toLocalDate(); + if (minDay == null || d.isBefore(minDay)) { + minDay = d; + } + if (maxDay == null || d.isAfter(maxDay)) { + maxDay = d; + } + } + Map existing = new HashMap<>(); + for (JYL row : jylMapper.selectList(new LambdaQueryWrapper() + .in(JYL::getJybh, jybhSet) + .ge(JYL::getRq, minDay.atStartOfDay()) + .lt(JYL::getRq, maxDay.plusDays(1).atStartOfDay()))) { + if (row == null || row.getRq() == null || row.getJc() == null) { + continue; + } + existing.put(cellKey(row.getJybh(), row.getRq().toLocalDate(), row.getJc()), row); + } + int affected = 0; + for (JYL item : list) { + JYL old = existing.get(cellKey(item.getJybh(), item.getRq().toLocalDate(), item.getJc())); + if (old == null) { + if (item.getKpk() == null) { + // 教员历的语义是"标出不可排课的时段",新标记默认不可排课 + item.setKpk(0); + } + add(item); + } else { + if (item.getKpk() != null) { + old.setKpk(item.getKpk()); + } + if (StringUtils.isNotEmpty(item.getMc())) { + old.setMc(item.getMc()); + } + old.setBz(item.getBz()); + update(old); + } + affected++; + } + return affected; + } + + @Override + public int batchDelete(List bhs) { + if (bhs == null || bhs.isEmpty()) { + return 0; + } + List ids = bhs.stream().filter(StringUtils::isNotEmpty).collect(java.util.stream.Collectors.toList()); + if (ids.isEmpty()) { + return 0; + } + return jylMapper.delete(new LambdaQueryWrapper().in(JYL::getBh, ids)); + } + + /** 判重键:教员编号|yyyy-MM-dd|节次 */ + private static String cellKey(String jybh, LocalDate day, Integer jc) { + return (jybh == null ? "" : jybh.trim()) + "|" + day + "|" + jc; + } + private LambdaQueryWrapper buildWrapper(JYL condition) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); if (condition != null) { diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/RunningCoursePublishServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/RunningCoursePublishServiceImpl.java index c3519f7a..83956697 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/RunningCoursePublishServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/RunningCoursePublishServiceImpl.java @@ -138,7 +138,10 @@ public class RunningCoursePublishServiceImpl implements RunningCoursePublishServ for (List group : groups.values()) { XYDRWB head = group.get(0); SSKC sskc = new SSKC(); - sskc.setBh(UuidUtil.getUUID()); + // 实施_课程.编号 是 VARBINARY(16):MyBatis 走 @TableId 元数据不带 typeHandler, + // 必须绑 32 位无横线十六进制,带横线会被达梦当普通字符串写入并报 String truncated。 + String sskcBh = UuidUtil.getUUID(); + sskc.setBh(sskcBh); sskc.setNd(ndCode); sskc.setJybh(head.getJybh()); sskc.setKmbh(head.getKbh()); @@ -153,12 +156,27 @@ public class RunningCoursePublishServiceImpl implements RunningCoursePublishServ sskc.setBjrxypjf(head.getBjrxypjf()); sskc.setCjsj(now); sskc.setBdsj(now); + // 以下非空列必须显式赋值(实施_课程共 26 个 NOT NULL 列,无库级缺省) + sskc.setJykcxh(1); + sskc.setKcksxs(1.0f); + sskc.setKcxycpzt("未开始"); + sskc.setKbbdsj(now); + sskc.setKhxs(head.getKsks() == null ? 0 : head.getKsks()); + sskc.setYxxs(0); + sskc.setYxllxs(0); + sskc.setYxsjxs(0); + sskc.setYxkhxs(0); + sskc.setPxxhdf(0f); + sskc.setQrpxzh(0); + sskc.setZrjybh(head.getJybh()); + sskc.setJypxjszt(0); sskcMapper.insert(sskc); for (XYDRWB task : group) { SSKCXYD link = new SSKCXYD(); - link.setBh(UuidUtil.getUUID()); - link.setSskcbh(sskc.getBh()); + // 实施_课程学员队.编号/实施_课程编号 是 CHAR(36),约定存带横线 36 位 UUID + link.setBh(UuidUtil.getOriginalUUID()); + link.setSskcbh(UuidUtil.fromBytes(UuidUtil.toBytes(sskcBh))); link.setXydbh(task.getXydbh()); link.setJc(team != null ? team.getJc() : null); link.setRs(team != null ? team.getXydrs() : null); @@ -213,7 +231,9 @@ public class RunningCoursePublishServiceImpl implements RunningCoursePublishServ Long remain = sskcxydMapper.selectCount(new LambdaQueryWrapper() .eq(SSKCXYD::getSskcbh, sskcbh)); if (remain == null || remain == 0) { - sskcMapper.deleteById(sskcbh); + // 注意:实施_课程.编号为 VARBINARY GUID,deleteById 直绑字符串会报 + // dm.jdbc.driver.DMException: Invalid hexadecimal digits,必须走 RAWTOHEX + sskcMapper.deleteByBhs(java.util.List.of(sskcbh)); } removed++; } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SchedulingWindowServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SchedulingWindowServiceImpl.java index 3506c585..42e9f27b 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SchedulingWindowServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/SchedulingWindowServiceImpl.java @@ -5,10 +5,16 @@ import com.roomroot.common.exception.ServiceException; import com.roomroot.common.utils.SecurityUtils; import com.roomroot.jwgl.dto.scheduling.SchedulingArrangeRequest; import com.roomroot.jwgl.dto.scheduling.SchedulingCellOpRequest; +import com.roomroot.jwgl.dto.scheduling.SchedulingCourseEditRequest; +import com.roomroot.jwgl.entity.BZLB; +import com.roomroot.jwgl.entity.BZTGMX; +import com.roomroot.jwgl.entity.JCSJB; import com.roomroot.jwgl.entity.JQB; +import com.roomroot.jwgl.entity.JSB; import com.roomroot.jwgl.entity.JYB; import com.roomroot.jwgl.entity.JXCDL; import com.roomroot.jwgl.entity.JYL; +import com.roomroot.jwgl.entity.KCBBZMX; import com.roomroot.jwgl.entity.KCJXYX_CZRZ; import com.roomroot.jwgl.entity.SSKC; import com.roomroot.jwgl.entity.SSKCB; @@ -18,7 +24,13 @@ import com.roomroot.jwgl.entity.SSKCBXYD; import com.roomroot.jwgl.entity.SSKCXYD; import com.roomroot.jwgl.entity.XYDB; import com.roomroot.jwgl.entity.XYDNDXQJBXXB; +import com.roomroot.jwgl.entity.XYDRWB; import com.roomroot.jwgl.entity.XQXLB; +import com.roomroot.jwgl.mapper.BZLBMapper; +import com.roomroot.jwgl.mapper.BZTGMXMapper; +import com.roomroot.jwgl.mapper.ClassRoomMapper; +import com.roomroot.jwgl.mapper.JCSJBMapper; +import com.roomroot.jwgl.mapper.KCBBZMXMapper; import com.roomroot.jwgl.mapper.XYDBMapper; import com.roomroot.jwgl.mapper.ClassRoomCalendarMapper; import com.roomroot.jwgl.mapper.ClassSemesterMapper; @@ -40,6 +52,7 @@ import com.roomroot.jwgl.utils.SemesterCodeUtil; import com.roomroot.jwgl.utils.PeriodUtil; import com.roomroot.jwgl.utils.UuidUtil; import com.roomroot.jwgl.vo.scheduling.SchedulingViewVO; +import com.roomroot.common.core.domain.entity.SysUser; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -52,6 +65,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -129,6 +143,24 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { @Resource private com.roomroot.jwgl.mapper.StudentTeamTaskMapper studentTeamTaskMapper; + @Resource + private JCSJBMapper jcsjbMapper; + + @Resource + private ClassRoomMapper classRoomMapper; + + @Resource + private BZLBMapper bzlbMapper; + + @Resource + private BZTGMXMapper bztgmxMapper; + + @Resource + private KCBBZMXMapper kcbbzMapper; + + @Resource + private com.roomroot.system.service.ISysUserService userService; + // ==================== 视图 ==================== @Override @@ -147,6 +179,13 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { List sskcIds = links.stream().map(SSKCXYD::getSskcbh).distinct().toList(); Map courses = sskcIds.isEmpty() ? Map.of() : sskcMapper.selectByBhs(sskcIds).stream().collect(Collectors.toMap(SSKC::getBh, c -> c, (a, b) -> a)); + // 编制侧任务(排课建议 / 简称 / 计划教员) + Map taskByKbh = new LinkedHashMap<>(); + for (XYDRWB task : studentTeamTaskMapper.selectList(new LambdaQueryWrapper() + .eq(XYDRWB::getXydxqbh, ctx.semester.getBh()) + .eq(XYDRWB::getDelFlag, 0))) { + taskByKbh.putIfAbsent(task.getKbh(), task); + } List lessons = ctx.lessons; Map> lessonsByCourse = lessons.stream() .collect(Collectors.groupingBy(SSKCB::getSskcbh)); @@ -162,6 +201,25 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { c.setZks(course.getZks()); c.setJybh(course.getJybh()); c.setJsbh(course.getJsbh()); + XYDRWB task = taskByKbh.get(course.getKmbh()); + if (task != null) { + c.setJc(task.getJc()); + c.setJysjhbz(task.getJysjhbz()); + c.setJysjhjybh(task.getJysjhjybh()); + c.setJysjhjyxm(resolveTeacherName(task.getJysjhjybh())); + } + if (c.getJybh() != null) c.setJyxm(resolveTeacherName(c.getJybh())); + if (c.getJsbh() != null) c.setJsmc(resolveRoomName(c.getJsbh())); + // ⑤ 合班信息:该运行课程关联的全部班次 + for (SSKCXYD link : links) { + if (!course.getBh().equals(link.getSskcbh())) continue; + SchedulingViewVO.Team t = new SchedulingViewVO.Team(); + t.setXydbh(link.getXydbh()); + t.setXydxqbh(ctx.semester.getBh()); + t.setXydmc(link.getXydbh() != null && ctx.team != null && ctx.team.getXydbh().equals(link.getXydbh()) + ? ctx.team.getXydmc() : resolveTeamName(link.getXydbh())); + c.getTeams().add(t); + } List cl = lessonsByCourse.getOrDefault(sskcbh, List.of()); c.setScheduledHours(cl.stream().mapToInt(l -> HOURS_PER_LESSON + (l.getJcdj() == null ? 0 : l.getJcdj())).sum()); c.setFull(course.getXs() != null && c.getScheduledHours() >= course.getXs()); @@ -213,14 +271,55 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { .collect(Collectors.groupingBy(l -> l.getRq().toLocalDate().toString() + "#" + l.getJc())); Map courseNames = new HashMap<>(); Map courseTeachers = new HashMap<>(); - for (SSKC course : sskcMapper.selectByBhs(ctx.links.stream().map(SSKCXYD::getSskcbh).distinct().toList())) { + List courseBhList = ctx.links.stream().map(SSKCXYD::getSskcbh).distinct().toList(); + for (SSKC course : courseBhList.isEmpty() ? List.of() : sskcMapper.selectByBhs(courseBhList)) { courseNames.put(course.getBh(), resolveCourseName(course.getKmbh())); - courseTeachers.put(course.getBh(), safe(course.getJybh())); + courseTeachers.put(course.getBh(), resolveTeacherName(course.getJybh())); + } + // 课次明细:学员队 / 教室 / 教员(供 ② 冲突显示选项与双击回填使用) + Map> teamsOfLesson = new HashMap<>(); + Map> roomsOfLesson = new HashMap<>(); + Map> teachersOfLesson = new HashMap<>(); + Map mainTeacherOfLesson = new HashMap<>(); + List allLessonIds = ctx.lessons.stream().map(SSKCB::getBh).distinct().toList(); + if (!allLessonIds.isEmpty()) { + for (SSKCBXYD l : sskcbxydMapper.selectList(new LambdaQueryWrapper() + .in(SSKCBXYD::getSskcbbh, allLessonIds))) { + teamsOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getXydbh()); + } + for (SSKCBJS l : sskcbjsMapper.selectList(new LambdaQueryWrapper() + .in(SSKCBJS::getSskcbbh, allLessonIds))) { + roomsOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getJsbh()); + } + for (SSKCBFZJY l : sskcbfzjyMapper.selectList(new LambdaQueryWrapper() + .in(SSKCBFZJY::getSskcbbh, allLessonIds))) { + teachersOfLesson.computeIfAbsent(l.getSskcbbh(), k -> new ArrayList<>()).add(l.getFzjybh()); + if (l.getZjy() != null && l.getZjy() == 1) { + mainTeacherOfLesson.put(l.getSskcbbh(), l.getFzjybh()); + } else { + mainTeacherOfLesson.putIfAbsent(l.getSskcbbh(), l.getFzjybh()); + } + } + } + // 场地历 / 教员历 不可用(按 date#jc 聚合,供冲突显示选项分类着色) + Map> blockedRoomsByCell = new HashMap<>(); + for (JXCDL row : classRoomCalendarMapper.selectList(new LambdaQueryWrapper() + .eq(JXCDL::getKpk, false).eq(JXCDL::getDelFlag, 0))) { + if (row.getRq() == null || row.getJsbh() == null) continue; + blockedRoomsByCell.computeIfAbsent(row.getRq().toLocalDate() + "#" + row.getJc(), k -> new ArrayList<>()) + .add(row.getJsbh()); + } + Map> blockedTeachersByCell = new HashMap<>(); + for (JYL row : jylMapper.selectList(new LambdaQueryWrapper().eq(JYL::getKpk, 0))) { + if (row.getRq() == null || row.getJybh() == null) continue; + blockedTeachersByCell.computeIfAbsent(row.getRq().toLocalDate() + "#" + row.getJc(), k -> new ArrayList<>()) + .add(row.getJybh()); } LocalDate weekStart = kxrq.with(DayOfWeek.MONDAY); int weekNo = 1; - int[] periodCount = {8}; + int[] periodCount = {resolvePeriodCount()}; + vo.setPeriods(resolvePeriods()); while (!weekStart.isAfter(jsrq)) { SchedulingViewVO.Week week = new SchedulingViewVO.Week(); week.setWeekNo(weekNo); @@ -238,23 +337,41 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { String key = date.toString() + "#" + jc; if (schoolHard.contains(key) || classHard.contains(key)) { cell.setUnavailable(true); - cell.setReason("不可排课(" + (schoolHard.contains(key) ? "校历" : "班历") + ")"); + cell.setCalendarKind(schoolHard.contains(key) ? "SCHOOL" : "CLASS"); + cell.setReason("不可排课(" + ("SCHOOL".equals(cell.getCalendarKind()) ? "校历" : "班历") + ")"); } else { if (schoolSoft.contains(key) || classSoft.contains(key)) { cell.setWarning("非正课时段"); + cell.setSoft(true); } cell.setUnavailable(false); } + cell.setBlockedRooms(blockedRoomsByCell.getOrDefault(key, new ArrayList<>())); + cell.setBlockedTeachers(blockedTeachersByCell.getOrDefault(key, new ArrayList<>())); for (SSKCB lesson : lessonsByGrid.getOrDefault(key, List.of())) { SchedulingViewVO.Lesson l = new SchedulingViewVO.Lesson(); l.setBh(lesson.getBh()); l.setSskcbh(lesson.getSskcbh()); l.setKcmc(courseNames.getOrDefault(lesson.getSskcbh(), "-")); l.setJxnr(lesson.getJxnr()); + l.setJxyd(lesson.getJxyd()); l.setJxff(lesson.getJxff()); l.setJcdj(lesson.getJcdj()); l.setYcxx(lesson.getYcxx()); + l.setJxbzbz(lesson.getJxbzbz()); + l.setBzms(lesson.getBzms()); l.setJyxm(courseTeachers.getOrDefault(lesson.getSskcbh(), "-")); + l.setJybh(mainTeacherOfLesson.get(lesson.getBh())); + if (l.getJybh() != null) { + l.setJyxm(resolveTeacherName(l.getJybh())); + } + l.setXydbhs(teamsOfLesson.getOrDefault(lesson.getBh(), new ArrayList<>())); + l.setJsbhs(roomsOfLesson.getOrDefault(lesson.getBh(), new ArrayList<>())); + l.setJybhs(teachersOfLesson.getOrDefault(lesson.getBh(), new ArrayList<>())); + if (!l.getJsbhs().isEmpty()) { + l.setJsbh(l.getJsbhs().get(0)); + l.setJsmc(resolveRoomName(l.getJsbhs().get(0))); + } cell.getLessons().add(l); } day.getCells().add(cell); @@ -268,6 +385,34 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { vo.setTotalWeeks(weekNo - 1); } + /** 节次字典:节次时间表展开为 1..N,供前端显示控制 */ + private List resolvePeriods() { + List periods = new ArrayList<>(); + try { + for (JCSJB row : jcsjbMapper.selectList(new LambdaQueryWrapper())) { + String label = row.getJc() != null && !row.getJc().isEmpty() ? row.getJc() : row.getJcsy(); + for (Integer jc : PeriodUtil.parsePeriods(row.getJcsy())) { + if (periods.stream().anyMatch(p -> p.getJc().equals(jc))) continue; + SchedulingViewVO.Period p = new SchedulingViewVO.Period(); + p.setJc(jc); + p.setLabel(label); + p.setSd(row.getSd()); + periods.add(p); + } + } + } catch (Exception e) { + log.debug("[排课窗] 读取节次字典失败,退化为默认 1-8: {}", e.getMessage()); + } + periods.sort(Comparator.comparing(SchedulingViewVO.Period::getJc)); + return periods; + } + + /** 最大节次数:优先取节次时间表,缺省 8 */ + private int resolvePeriodCount() { + List periods = resolvePeriods(); + return periods.isEmpty() ? 8 : periods.get(periods.size() - 1).getJc(); + } + // ==================== 安排所选节次 ==================== @Override @@ -281,11 +426,51 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { if (!canEdit(course, ctx)) { throw new ServiceException("没有该课程的排课权限", FORBIDDEN); } - String jsbh = request.getJsbh() != null && !request.getJsbh().isEmpty() ? request.getJsbh() : course.getJsbh(); - if (jsbh == null || jsbh.isEmpty()) { + // 教学场地(支持多场地):jsbhs > jsbh > 课程默认场地 + List rooms = new ArrayList<>(); + if (request.getJsbhs() != null) { + request.getJsbhs().stream().filter(s -> s != null && !s.isEmpty()).distinct().forEach(rooms::add); + } + if (rooms.isEmpty() && request.getJsbh() != null && !request.getJsbh().isEmpty()) { + rooms.add(request.getJsbh()); + } + if (rooms.isEmpty() && course.getJsbh() != null && !course.getJsbh().isEmpty()) { + rooms.add(course.getJsbh()); + } + if (rooms.isEmpty()) { throw new ServiceException("请指定场地(课程未设置默认场地)", BAD_REQUEST); } + String jsbh = rooms.get(0); String jybh = request.getJybh() != null && !request.getJybh().isEmpty() ? request.getJybh() : course.getJybh(); + List fzjybhs = request.getFzjybhs() == null ? List.of() + : request.getFzjybhs().stream().filter(s -> s != null && !s.isEmpty()).distinct().toList(); + // 教学保障:数量不得超过资源库总量 + List supportRows = new ArrayList<>(); + if (request.getSupports() != null) { + for (SchedulingArrangeRequest.Support s : request.getSupports()) { + if (s == null || s.getBztgmxbh() == null || s.getBztgmxbh().isEmpty()) continue; + if (s.getSl() == null || s.getSl() <= 0) { + throw new ServiceException("教学保障需求数量必须大于 0", BAD_REQUEST); + } + BZTGMX res = bztgmxMapper.selectById(s.getBztgmxbh()); + if (res == null) { + throw new ServiceException("教学保障资源不存在:" + s.getBztgmxbh(), BAD_REQUEST); + } + if (res.getSl() != null && s.getSl() > res.getSl()) { + throw new ServiceException("教学保障「" + res.getMc() + "」需求数量 " + s.getSl() + + " 超过总量 " + res.getSl() + (res.getDw() == null ? "" : res.getDw()), BAD_REQUEST); + } + KCBBZMX row = new KCBBZMX(); + row.setBztgmxbh(s.getBztgmxbh()); + row.setSl(s.getSl()); + row.setBz(s.getBz()); + row.setNd(ctx.ndCode); + supportRows.add(row); + } + } + // 教学班次(可临时增删):为空用课程的全部合班班次 + List xydbhs = request.getXydbhs() == null ? List.of() + : request.getXydbhs().stream().filter(s -> s != null && !s.isEmpty()).distinct().toList(); // 硬冲突判定所需数据 List semesterLessons = ctx.lessons; @@ -351,9 +536,9 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { ignored.add(ignoredCell(cell, "班历不可排课")); continue; } - String roomKey = key + "#" + jsbh; - if (roomBlocked.containsKey(roomKey)) { - ignored.add(ignoredCell(cell, "场地历不可用")); + String blockedRoom = rooms.stream().filter(r -> roomBlocked.containsKey(key + "#" + r)).findFirst().orElse(null); + if (blockedRoom != null) { + ignored.add(ignoredCell(cell, "场地历不可用(教室 " + blockedRoom + ")")); continue; } if (jybh != null && teacherBlocked.containsKey(key + "#" + jybh)) { @@ -375,7 +560,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { busy = true; busyReason = "班次该节已有其它课程"; break; } - if (roomsOfLesson.getOrDefault(occupant.getBh(), List.of()).contains(jsbh)) { + if (roomsOfLesson.getOrDefault(occupant.getBh(), List.of()).stream().anyMatch(rooms::contains)) { busy = true; busyReason = "教室该节已被占用"; break; } @@ -396,7 +581,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { } if (!ownOccupied) { SSKCB lesson = new SSKCB(); - lesson.setBh(UuidUtil.getUUID()); + lesson.setBh(UuidUtil.getOriginalUUID()); lesson.setSskcbh(course.getBh()); lesson.setJhsjap(0); lesson.setSjsjap(0); @@ -415,39 +600,92 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { lesson.setCjsj(now); lesson.setBdsj(now); sskcbMapper.insert(lesson); - // 学员队 / 教室 / 主讲教员 - for (SSKCXYD link : ctx.links) { - if (!course.getBh().equals(link.getSskcbh())) continue; - SSKCBXYD lxyd = new SSKCBXYD(); - lxyd.setBh(UuidUtil.getUUID()); - lxyd.setSskcbbh(lesson.getBh()); - lxyd.setXydbh(link.getXydbh()); - lxyd.setRs(link.getRs()); - lxyd.setJc(link.getJc()); - lxyd.setNd(ctx.ndCode); - lxyd.setRq(lesson.getRq()); - lxyd.setJc2(cell.getJc()); - sskcbxydMapper.insert(lxyd); + // 学员队(教学班次,可临时增删) + if (xydbhs.isEmpty()) { + for (SSKCXYD link : ctx.links) { + if (!course.getBh().equals(link.getSskcbh())) continue; + SSKCBXYD lxyd = new SSKCBXYD(); + lxyd.setBh(UuidUtil.getOriginalUUID()); + lxyd.setSskcbbh(lesson.getBh()); + lxyd.setXydbh(link.getXydbh()); + lxyd.setRs(link.getRs()); + lxyd.setJc(link.getJc()); + lxyd.setNd(ctx.ndCode); + lxyd.setRq(lesson.getRq()); + lxyd.setJc2(cell.getJc()); + sskcbxydMapper.insert(lxyd); + } + } else { + for (String xydbh : xydbhs) { + SSKCXYD link = ctx.links.stream() + .filter(k -> course.getBh().equals(k.getSskcbh()) && xydbh.equals(k.getXydbh())) + .findFirst().orElse(null); + XYDB team = xydbMapper.selectById(xydbh); + SSKCBXYD lxyd = new SSKCBXYD(); + lxyd.setBh(UuidUtil.getOriginalUUID()); + lxyd.setSskcbbh(lesson.getBh()); + lxyd.setXydbh(xydbh); + lxyd.setRs(link != null ? link.getRs() : (team != null ? team.getXydrs() : null)); + lxyd.setJc(link != null ? link.getJc() : (team != null ? team.getJc() : null)); + lxyd.setNd(ctx.ndCode); + lxyd.setRq(lesson.getRq()); + lxyd.setJc2(cell.getJc()); + sskcbxydMapper.insert(lxyd); + } } - SSKCBJS ljs = new SSKCBJS(); - ljs.setBh(UuidUtil.getUUID()); - ljs.setSskcbbh(lesson.getBh()); - ljs.setJsbh(jsbh); - ljs.setNd(ctx.ndCode); - ljs.setRq(lesson.getRq()); - ljs.setJc(cell.getJc()); - sskcbjsMapper.insert(ljs); + // 教学场地(可多场地) + for (String room : rooms) { + SSKCBJS ljs = new SSKCBJS(); + ljs.setBh(UuidUtil.getOriginalUUID()); + ljs.setSskcbbh(lesson.getBh()); + ljs.setJsbh(room); + ljs.setNd(ctx.ndCode); + ljs.setRq(lesson.getRq()); + ljs.setJc(cell.getJc()); + sskcbjsMapper.insert(ljs); + } + // 授课教员:主讲 zjy=1 + 辅讲 zjy=0 if (jybh != null) { SSKCBFZJY lfz = new SSKCBFZJY(); - lfz.setBh(UuidUtil.getUUID()); + lfz.setBh(UuidUtil.getOriginalUUID()); lfz.setSskcbbh(lesson.getBh()); lfz.setFzjybh(jybh); lfz.setZjy(1); lfz.setNd(ctx.ndCode); + // 参与学员测评 / 学员测评平均分 为非空列:新排课次默认参与、分数待评 + lfz.setCyxycp(1); + lfz.setXycppjf(0f); lfz.setRq(lesson.getRq()); lfz.setJc(cell.getJc()); sskcbfzjyMapper.insert(lfz); } + for (String fzjybh : fzjybhs) { + if (fzjybh.equals(jybh)) continue; + SSKCBFZJY lfz = new SSKCBFZJY(); + lfz.setBh(UuidUtil.getOriginalUUID()); + lfz.setSskcbbh(lesson.getBh()); + lfz.setFzjybh(fzjybh); + lfz.setZjy(0); + lfz.setNd(ctx.ndCode); + lfz.setCyxycp(1); + lfz.setXycppjf(0f); + lfz.setRq(lesson.getRq()); + lfz.setJc(cell.getJc()); + sskcbfzjyMapper.insert(lfz); + } + // 教学保障明细 + for (KCBBZMX template : supportRows) { + KCBBZMX row = new KCBBZMX(); + row.setBh(UuidUtil.getOriginalUUID()); + row.setKcbbh(lesson.getBh()); + row.setBztgmxbh(template.getBztgmxbh()); + row.setSl(template.getSl()); + row.setBz(template.getBz()); + row.setNd(template.getNd()); + row.setRq(lesson.getRq()); + row.setJc(cell.getJc()); + kcbbzMapper.insert(row); + } arranged++; logOperation(course, "安排", "安排 " + cell.getRq() + " 第" + cell.getJc() + "节" + (ownOccupied ? "(覆盖原课次)" : ""), @@ -551,7 +789,8 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { } sskcxydMapper.delete(new LambdaQueryWrapper() .eq(SSKCXYD::getSskcbh, course.getBh())); - sskcMapper.deleteById(course.getBh()); + // 实施_课程.编号为 VARBINARY GUID,deleteById 会报 Invalid hexadecimal digits,须走 RAWTOHEX + sskcMapper.deleteByBhs(java.util.List.of(course.getBh())); logOperation(course, "删除", "彻底删除运行课程", null); return 1; } @@ -570,11 +809,160 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { m.put("czlx", r.getCzlx()); m.put("cznr", r.getCznr()); m.put("czrbh", r.getCzrbh()); + m.put("czrmc", resolveOperatorName(r.getCzrbh())); m.put("czsj", r.getCzsj()); return m; }).collect(Collectors.toList()); } + /** 操作人显示名:按登录名取用户昵称,取不到则回显登录名 */ + private String resolveOperatorName(String username) { + if (username == null || username.isEmpty()) return "-"; + try { + SysUser user = userService.selectUserByUserName(username); + if (user != null && user.getNickName() != null) { + return user.getNickName(); + } + } catch (Exception ignore) { + // 取不到昵称时回显登录名 + } + return username; + } + + // ==================== 编辑课程教学任务信息(手册 12.3.1) ==================== + + @Override + @Transactional(rollbackFor = Exception.class) + public Map updateCourse(SchedulingCourseEditRequest request) { + if (request == null || request.getSskcbh() == null || request.getSskcbh().isEmpty()) { + throw new ServiceException("请指定运行课程", BAD_REQUEST); + } + SSKC course = requireCourse(request.getSskcbh()); + Ctx ctx = loadCtxForCourse(course); + if (!canEdit(course, ctx)) { + throw new ServiceException("没有该课程的信息修改权限", FORBIDDEN); + } + boolean changed = false; + Integer newXs = null; + Integer newZks = null; + String newJy = null; + String newJs = null; + if (request.getXs() != null && !request.getXs().equals(course.getXs())) { + newXs = request.getXs(); changed = true; + } + if (request.getZks() != null && !request.getZks().equals(course.getZks())) { + newZks = request.getZks(); changed = true; + } + if (request.getJybh() != null && !request.getJybh().isEmpty() && !request.getJybh().equals(course.getJybh())) { + newJy = request.getJybh(); changed = true; + } + if (request.getJsbh() != null && !request.getJsbh().isEmpty() && !request.getJsbh().equals(course.getJsbh())) { + newJs = request.getJsbh(); changed = true; + } + if (changed) { + // 实施_课程.编号为 VARBINARY GUID,updateById 会对主键绑字符串而报 Invalid hexadecimal digits + sskcMapper.updateBasicByBh(course.getBh(), newXs, newZks, newJy, newJs); + } + // 课程简称回写编制侧任务表(同一班次学期 + 同一课程编号) + if (request.getJc() != null && ctx.semester != null) { + for (XYDRWB task : studentTeamTaskMapper.selectList(new LambdaQueryWrapper() + .eq(XYDRWB::getXydxqbh, ctx.semester.getBh()) + .eq(XYDRWB::getKbh, course.getKmbh()) + .eq(XYDRWB::getDelFlag, 0))) { + task.setJc(request.getJc()); + task.setBdsj(LocalDateTime.now()); + studentTeamTaskMapper.updateById(task); + changed = true; + } + } + if (changed) { + logOperation(course, "修改", "编辑课程教学任务信息(学时=" + (newXs != null ? newXs : course.getXs()) + + ",周课时=" + (newZks != null ? newZks : course.getZks()) + + ",简称=" + request.getJc() + ")", null); + } + Map result = new HashMap<>(); + result.put("updated", changed); + return result; + } + + // ==================== 教学班次(合班)与候选班次(手册 12.3.2.2) ==================== + + @Override + public Map courseTeams(String sskcbh) { + SSKC course = requireCourse(sskcbh); + Ctx ctx = loadCtxForCourse(course); + Map result = new HashMap<>(); + // 当前教学班次:该运行课程关联的学员队 + List> current = new ArrayList<>(); + for (SSKCXYD link : ctx.links) { + if (!course.getBh().equals(link.getSskcbh())) continue; + Map m = new HashMap<>(); + m.put("xydbh", link.getXydbh()); + m.put("xydmc", resolveTeamName(link.getXydbh())); + m.put("rs", link.getRs()); + current.add(m); + } + // 候选班次:同学期开设同一课程编号的其它班次学期 + List> candidates = new ArrayList<>(); + java.util.LinkedHashSet seen = new java.util.LinkedHashSet<>(); + for (XYDRWB task : studentTeamTaskMapper.selectList(new LambdaQueryWrapper() + .eq(XYDRWB::getKbh, course.getKmbh()) + .eq(XYDRWB::getDelFlag, 0))) { + if (Objects.equals(task.getXydxqbh(), ctx.semester == null ? null : ctx.semester.getBh())) continue; + if (task.getXydxqbh() == null || !seen.add(task.getXydxqbh())) continue; + XYDNDXQJBXXB semester = classSemesterMapper.selectById(task.getXydxqbh()); + if (semester == null) continue; + Map m = new HashMap<>(); + m.put("xydbh", task.getXydbh()); + m.put("xydxqbh", task.getXydxqbh()); + m.put("xydmc", resolveTeamName(task.getXydbh())); + m.put("nd", semester.getNd()); + m.put("xqdc", semester.getXqdc()); + candidates.add(m); + } + result.put("current", current); + result.put("candidates", candidates); + return result; + } + + // ==================== 教学保障选项(手册 12.3.2.5) ==================== + + @Override + public Map supportOptions() { + Map result = new HashMap<>(); + List> categories = new ArrayList<>(); + try { + for (BZLB lb : bzlbMapper.selectList(new LambdaQueryWrapper())) { + if (lb.getTy() != null && lb.getTy() != 0) continue; + Map m = new HashMap<>(); + m.put("bh", lb.getBh()); + m.put("mc", lb.getMc()); + categories.add(m); + } + } catch (Exception e) { + log.debug("[排课窗] 读取保障类别失败: {}", e.getMessage()); + } + List> resources = new ArrayList<>(); + try { + for (BZTGMX mx : bztgmxMapper.selectList(new LambdaQueryWrapper())) { + if (mx.getTy() != null && mx.getTy() != 0) continue; + Map m = new HashMap<>(); + m.put("bh", mx.getBh()); + m.put("mc", mx.getMc()); + m.put("dw", mx.getDw()); + m.put("total", mx.getSl()); + m.put("bzlbbh", mx.getBzlbbh()); + m.put("zylb", mx.getZylb()); + resources.add(m); + } + } catch (Exception e) { + log.debug("[排课窗] 读取保障资源失败: {}", e.getMessage()); + } + result.put("categories", categories); + result.put("resources", resources); + return result; + } + // ==================== 公共 ==================== /** 上下文:班次学期 + 学员队 + 6 位代号 + 本班次已发布课程与课次 */ @@ -688,6 +1076,7 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { sskcbxydMapper.delete(new LambdaQueryWrapper().eq(SSKCBXYD::getSskcbbh, sskcbbh)); sskcbjsMapper.delete(new LambdaQueryWrapper().eq(SSKCBJS::getSskcbbh, sskcbbh)); sskcbfzjyMapper.delete(new LambdaQueryWrapper().eq(SSKCBFZJY::getSskcbbh, sskcbbh)); + kcbbzMapper.delete(new LambdaQueryWrapper().eq(KCBBZMX::getKcbbh, sskcbbh)); } /** 校历/班历不可排格键集合 */ @@ -757,7 +1146,8 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { private void logOperation(SSKC course, String czlx, String cznr, SchedulingArrangeRequest ignored) { try { KCJXYX_CZRZ row = new KCJXYX_CZRZ(); - row.setBh(UuidUtil.getUUID()); + // 课程教学运行_操作日志.编号 为 CHAR(36),按项目约定存带横线 36 位 UUID + row.setBh(UuidUtil.getOriginalUUID()); row.setTybh(course.getBh()); row.setCzlx(czlx); row.setCznr(cznr); @@ -779,6 +1169,39 @@ public class SchedulingWindowServiceImpl implements SchedulingWindowService { return kb != null && kb.getKmc() != null ? kb.getKmc() : kbh; } + /** 教员姓名(失败返回编号) */ + private String resolveTeacherName(String jybh) { + if (jybh == null || jybh.isEmpty()) return null; + try { + JYB jyb = jybMapper.selectById(jybh); + return jyb != null && jyb.getJyxm() != null ? jyb.getJyxm() : jybh; + } catch (Exception e) { + return jybh; + } + } + + /** 教室名称(失败返回编号) */ + private String resolveRoomName(String jsbh) { + if (jsbh == null || jsbh.isEmpty()) return null; + try { + List rooms = classRoomMapper.selectList(new LambdaQueryWrapper().eq(JSB::getJsbh, jsbh)); + return rooms.isEmpty() ? jsbh : (rooms.get(0).getJsmc() != null ? rooms.get(0).getJsmc() : jsbh); + } catch (Exception e) { + return jsbh; + } + } + + /** 学员队名称(失败返回编号) */ + private String resolveTeamName(String xydbh) { + if (xydbh == null || xydbh.isEmpty()) return null; + try { + XYDB team = xydbMapper.selectById(xydbh); + return team != null && team.getXydmc() != null ? team.getXydmc() : xydbh; + } catch (Exception e) { + return xydbh; + } + } + private static String intDate(Integer d) { String s = String.valueOf(d); return s.length() == 8 ? s.substring(0, 4) + "-" + s.substring(4, 6) + "-" + s.substring(6, 8) : s; diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/StudentTeamTaskServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/StudentTeamTaskServiceImpl.java index 4227b6f5..2ec4b34e 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/StudentTeamTaskServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/StudentTeamTaskServiceImpl.java @@ -595,8 +595,11 @@ public class StudentTeamTaskServiceImpl implements StudentTeamTaskService { if (row.getKbh() == null || row.getXydbh() == null) { return false; } - List courses = sskcMapper.selectList(new LambdaQueryWrapper().eq(SSKC::getKmbh, row.getKbh())); + // 注意:实施_课程 为中文列名 + VARBINARY 主键,selectList 会整行返回 null 元素, + // 这里必须走显式 XML(否则下面的 course.getBh() 会 NPE)。 + List courses = sskcMapper.selectByKmbh(row.getKbh()); for (SSKC course : courses) { + if (course == null) continue; Long linked = sskcxydMapper.selectCount(new LambdaQueryWrapper() .eq(SSKCXYD::getSskcbh, course.getBh()) .eq(SSKCXYD::getXydbh, row.getXydbh())); diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TaskBookFillServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TaskBookFillServiceImpl.java index a5f80e56..5577dd37 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TaskBookFillServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TaskBookFillServiceImpl.java @@ -8,10 +8,12 @@ import com.roomroot.jwgl.dto.taskbook.TaskBookRoomRequest; import com.roomroot.jwgl.dto.taskbook.TaskBookTeacherRequest; import com.roomroot.jwgl.entity.JYB; import com.roomroot.jwgl.entity.JXRW; +import com.roomroot.jwgl.entity.SSKCXYD; import com.roomroot.jwgl.entity.XYDNDXQJBXXB; import com.roomroot.jwgl.entity.XYDRWB; import com.roomroot.jwgl.mapper.ClassSemesterMapper; import com.roomroot.jwgl.mapper.JYBMapper; +import com.roomroot.jwgl.mapper.SSKCXYDMapper; import com.roomroot.jwgl.mapper.StudentTeamTaskMapper; import com.roomroot.jwgl.mapper.TeachingTaskMapper; import com.roomroot.jwgl.service.TaskBookFillService; @@ -25,10 +27,12 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import static com.roomroot.jwgl.utils.BasicManagementConstants.BAD_REQUEST; import static com.roomroot.jwgl.utils.BasicManagementConstants.CONFLICT; @@ -55,6 +59,9 @@ public class TaskBookFillServiceImpl implements TaskBookFillService { @Resource private TeachingTaskWriteGuard guard; + @Resource + private SSKCXYDMapper sskcxydMapper; + // ==================== 列表 ==================== @Override @@ -191,7 +198,8 @@ public class TaskBookFillServiceImpl implements TaskBookFillService { public int split(TaskBookBhListRequest request) { List rows = loadRows(request); assertAllFillable(rows); - // TODO(阶段 5):行已发布到运行课表的须先撤回,再拆班。阶段 5 落地后在此校验。 + // 阶段 5 已落地:行所属班次学期若已发布到运行课表,合班组已被固化成运行课程,必须先撤回。 + assertNotPublishedToRunning(rows); LocalDateTime now = LocalDateTime.now(); int count = 0; for (XYDRWB row : rows) { @@ -375,4 +383,33 @@ public class TaskBookFillServiceImpl implements TaskBookFillService { assertFillable(row); } } + + /** + * 阶段 5 反向门禁:拆班前确认行所在班次学期尚未发布到运行课表。 + * + *

    判定信号与 {@code RunningCoursePublishServiceImpl#publish} 的幂等门禁同源: + * 「实施_课程学员队」按 (学员队编号, 年度=6 位学期代号) 命中即说明该班次学期已整批发布。 + * 发布后合班组已固化为运行课程的 实施_课程.编号,拆班会让任务书与运行课表不一致。

    + * + *

    不用「实施_课程表.教学保障备注」或「课表变动时间」判定:前者由 logOperation 只更新自己 + * 那门课,后者发布时不写,两者都不可靠。

    + */ + private void assertNotPublishedToRunning(List rows) { + Set checked = new HashSet<>(); + for (XYDRWB row : rows) { + String xydxqbh = row.getXydxqbh(); + if (xydxqbh == null || xydxqbh.isEmpty() || !checked.add(xydxqbh)) { + continue; + } + XYDNDXQJBXXB semester = requireSemester(xydxqbh); + Integer ndCode = SemesterCodeUtil.resolve(semester.getNd(), semester.getXqdc()); + Long published = sskcxydMapper.selectCount(new LambdaQueryWrapper() + .eq(SSKCXYD::getXydbh, semester.getXydbh()) + .eq(SSKCXYD::getNd, ndCode)); + if (published != null && published > 0) { + throw new ServiceException("班次学期 " + semester.getXydbh() + "(" + ndCode + ")已发布 " + + published + " 条运行课程,请先在运行课表撤回后再拆班", CONFLICT); + } + } + } } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TimetableConflictServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TimetableConflictServiceImpl.java index 8a8e01ab..259ea5ef 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TimetableConflictServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/TimetableConflictServiceImpl.java @@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.roomroot.jwgl.entity.JQB; import com.roomroot.jwgl.entity.JXCDL; +import com.roomroot.jwgl.entity.JYL; import com.roomroot.jwgl.entity.SSKC; import com.roomroot.jwgl.entity.SSKCB; +import com.roomroot.jwgl.entity.SSKCBFZJY; import com.roomroot.jwgl.entity.SSKCXYD; import com.roomroot.jwgl.entity.TimetableConflictResult; import com.roomroot.jwgl.entity.XYDB; @@ -14,6 +16,7 @@ import com.roomroot.jwgl.entity.XQXLB; import com.roomroot.jwgl.mapper.ClassRoomCalendarMapper; import com.roomroot.jwgl.mapper.ClassSemesterMapper; import com.roomroot.jwgl.mapper.JQBMapper; +import com.roomroot.jwgl.mapper.JYLMapper; import com.roomroot.jwgl.mapper.KBMapper; import com.roomroot.jwgl.mapper.SSKCBFZJYMapper; import com.roomroot.jwgl.mapper.SSKCBJSMapper; @@ -96,6 +99,9 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { @Resource private ClassRoomCalendarMapper classRoomCalendarMapper; + @Resource + private JYLMapper jylMapper; + @Resource private SSKCBMapper sskcbMapper; @@ -258,7 +264,13 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { details = switch (dimension) { case TEAM_CONFLICT -> sskcbxydMapper.selectTeamConflictDetails(nd); case ELECTIVE_REQUIRED_CONFLICT -> sskcbxydMapper.selectElectiveRequiredConflictDetails(nd); - case TEACHER_CONFLICT -> sskcbfzjyMapper.selectTeacherConflictDetails(nd); + case TEACHER_CONFLICT -> { + // 口径统一(与排课窗口硬冲突一致):教员时间冲突 = 教员双占 + 教员历不可排 + List teacherDup = + new ArrayList<>(sskcbfzjyMapper.selectTeacherConflictDetails(nd)); + teacherDup.addAll(checkTeacherCalendarConflicts(nd)); + yield teacherDup; + } case CLASSROOM_CONFLICT -> { // 阶段 5.1:教室重复占用 + 场地历不可用 List dup = sskcbjsMapper.selectClassroomConflictDetails(nd); @@ -626,17 +638,92 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { return result; } - /** 构建 不可排课格 类冲突明细 */ + /** + * TEACHER_CONFLICT 追加:教员历 标记不可排课(可排课=0)但该教员该格已有课次。 + *

    + * 与 {@link #checkClassroomCalendarConflicts} 同为"不可排课格"类检查,只是主体换成教员: + * 排课窗口硬冲突里含「教员历不可用」,批量检查此前缺失该维度,此处补齐以保证两套口径一致。 + *

    + *

    + * 匹配口径与排课窗口一致:按 日期(天) + 节次 + 教员编号 三元组,不比对具体时分。 + *

    + */ + private List checkTeacherCalendarConflicts(Integer nd) { + List result = new ArrayList<>(); + List lessons = sskcbMapper.selectList(new LambdaQueryWrapper() + .eq(SSKCB::getNd, nd) + .eq(SSKCB::getSczt, 0) + .isNotNull(SSKCB::getRq)); + if (lessons.isEmpty()) { + return result; + } + // 教员历 无"删除状态"列,只需 可排课 = 0 + List unavailable = jylMapper.selectList(new LambdaQueryWrapper() + .eq(JYL::getKpk, 0) + .isNotNull(JYL::getRq)); + if (unavailable.isEmpty()) { + return result; + } + Set lessonIds = lessons.stream().map(SSKCB::getBh).collect(Collectors.toSet()); + // 课次 → 教员编号集合(主讲 + 辅讲统一纳入检查) + Map> teachersOfLesson = new HashMap<>(); + for (SSKCBFZJY link : sskcbfzjyMapper.selectList(new LambdaQueryWrapper() + .in(SSKCBFZJY::getSskcbbh, lessonIds))) { + if (link.getFzjybh() == null) continue; + teachersOfLesson.computeIfAbsent(link.getSskcbbh(), k -> new ArrayList<>()).add(link.getFzjybh()); + } + if (teachersOfLesson.isEmpty()) { + return result; + } + Map courseNames = loadCourseNames(nd); + // 按 教员编号#日期#节次 → 该格已有课次,便于一次聚合出明细 + Map> lessonsByTeacherCell = new HashMap<>(); + Map lessonByBh = lessons.stream() + .collect(Collectors.toMap(SSKCB::getBh, l -> l, (a, b) -> a)); + for (Map.Entry> e : teachersOfLesson.entrySet()) { + SSKCB lesson = lessonByBh.get(e.getKey()); + if (lesson == null || lesson.getRq() == null || lesson.getJc() == null) continue; + String dateKey = lesson.getRq().toLocalDate().toString() + "#" + lesson.getJc(); + for (String jybh : e.getValue()) { + lessonsByTeacherCell.computeIfAbsent(jybh + "#" + dateKey, k -> new ArrayList<>()).add(lesson); + } + } + for (JYL row : unavailable) { + if (row.getRq() == null || row.getJybh() == null) continue; + String dateStr = row.getRq().toLocalDate().toString(); + String key = row.getJybh() + "#" + dateStr + "#" + row.getJc(); + List hit = lessonsByTeacherCell.get(key); + if (hit == null || hit.isEmpty()) continue; + TimetableConflictDetailVO d = buildEventDetail( + "TEACHERCAL|" + row.getJybh() + "|" + dateStr + "#" + row.getJc(), + safeName(row.getMc()), dateStr, row.getJc(), hit, courseNames, + "教员历标记该教员该时段不可排课,但已有课次安排", + TimetableConflictDimension.TEACHER_CONFLICT); + result.add(d); + } + return result; + } + + /** 构建 不可排课格 类冲突明细(默认归属"日历冲突"维度) */ private TimetableConflictDetailVO buildEventDetail(String resourceId, String resourceName, String dateStr, Integer jc, List lessons, Map courseNames, String reason) { + return buildEventDetail(resourceId, resourceName, dateStr, jc, lessons, courseNames, reason, + TimetableConflictDimension.EVENT_CONFLICT); + } + + /** 构建 不可排课格 类冲突明细(可指定归属维度,使明细归属与其所在卡片一致) */ + private TimetableConflictDetailVO buildEventDetail(String resourceId, String resourceName, + String dateStr, Integer jc, List lessons, + Map courseNames, String reason, + TimetableConflictDimension dimension) { String courses = lessons.stream() .map(l -> courseNames.getOrDefault(l.getSskcbh(), l.getSskcbh())) .distinct() .collect(Collectors.joining(";")); return TimetableConflictDetailVO.builder() - .dimensionCode("EVENT_CONFLICT") - .dimensionTitle("日历冲突") + .dimensionCode(dimension.getCode()) + .dimensionTitle(dimension.getTitle()) .resourceId(resourceId) .resourceName(resourceName) .rq(LocalDate.parse(dateStr).atStartOfDay()) @@ -652,13 +739,27 @@ public class TimetableConflictServiceImpl implements TimetableConflictService { /** 实施_课程编号 → 课程名称(经 实施_课程.科目编号 → 课表.课名称) */ private Map loadCourseNames(Integer nd) { Map names = new HashMap<>(); - List courses = sskcMapper.selectList(new LambdaQueryWrapper().eq(SSKC::getNd, nd)); - if (courses.isEmpty()) return names; - Set kbhs = courses.stream().map(SSKC::getKmbh).collect(Collectors.toSet()); - Map kbhNames = kbMapper.selectBatchIds(kbhs).stream() - .collect(Collectors.toMap(com.roomroot.jwgl.entity.KB::getKbh, k -> safeName(k.getKmc()), (a, b) -> a)); + // 必须走显式 XML:selectList 在本表(中文列名 + VARBINARY 主键)会整行返回 null 元素 + List courses = sskcMapper.selectByNd(nd); + if (courses == null || courses.isEmpty()) { + return names; + } + Set kbhs = courses.stream() + .filter(Objects::nonNull) + .map(SSKC::getKmbh) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map kbhNames = new HashMap<>(); + if (!kbhs.isEmpty()) { + for (com.roomroot.jwgl.entity.KB k : kbMapper.selectBatchIds(kbhs)) { + if (k == null || k.getKbh() == null) continue; + kbhNames.put(k.getKbh(), safeName(k.getKmc())); + } + } for (SSKC c : courses) { - names.put(c.getBh(), kbhNames.getOrDefault(c.getKmbh(), c.getKmbh())); + if (c == null) continue; + names.put(c.getBh(), c.getKmbh() == null ? safeName(c.getBh()) + : kbhNames.getOrDefault(c.getKmbh(), safeName(c.getKmbh()))); } return names; } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/unit/conflict/TimetableConflictDimension.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/unit/conflict/TimetableConflictDimension.java index 163dda98..ea8fb543 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/unit/conflict/TimetableConflictDimension.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/unit/conflict/TimetableConflictDimension.java @@ -52,33 +52,48 @@ public enum TimetableConflictDimension { /** * 教员时间冲突检查。 - *

    检查逻辑:同一教员(主讲+辅讲统一在此维度)在同一日期同一节次被安排多门课程 → 冲突

    + *

    检查逻辑(两部分,均为硬冲突): + *

      + *
    1. 同一教员(主讲+辅讲统一在此维度)在同一日期同一节次被安排多门课程 → 冲突
    2. + *
    3. 教员历(教员工作时间表)标记"可排课=0"的日期节次上,该教员仍有已排课次 → 冲突
    4. + *
    + *

    *

    涉及数据表: *

      *
    1. 实施_课程表 SSKCB —— 主表,日期/节次/年度/删除状态
    2. *
    3. 实施_课程表_辅助教员 SSKCBFZJY —— 课次与教员的关联表(含主讲ZJY=1 与辅讲ZJY=0),获取教员编号(FZJYBH)
    4. + *
    5. 教员历 JYL —— 获取教员"可排课"标记(可排课=0 表示不可排)
    6. *
    7. 教员表 JYB / 教研室人员 JYSX —— 翻译中文姓名展示
    8. *
    *

    + *

    口径说明:排课窗口的硬冲突包含「教员历不可用」,为此本维度同样纳入教员历检查, + * 使批量冲突检查与排课窗口判断一致。实现方式与「场地历不可用」并入 + * {@link #CLASSROOM_CONFLICT} 保持一致,不再单开卡片,避免同一事实被重复计数。

    */ TEACHER_CONFLICT("TEACHER_CONFLICT", "教员时间冲突检查", - "同一教员在同一时间段被安排多门课程"), + "同一教员在同一时间段被安排多门课程,或教员历不可排课时段已有课程"), /** * 教室时间冲突检查。 - *

    检查逻辑:同一教室(场地)在同一日期同一节次被多门课程占用 → 冲突

    + *

    检查逻辑(两部分,均为硬冲突): + *

      + *
    1. 同一教室(场地)在同一日期同一节次被多门课程占用 → 冲突
    2. + *
    3. 教学场地历标记"可排课=0"的教室节次上仍有已排课次 → 冲突(场地历不可用)
    4. + *
    + *

    *

    涉及数据表: *

      *
    1. 实施_课程表 SSKCB —— 主表,日期/节次/年度/删除状态
    2. *
    3. 实施_课程表_教室 SSKCBJS —— 课次与教室的关联表,获取教室编号(JSBH)
    4. + *
    5. 教学场地历 JXCDL —— 获取场地"可排课"标记
    6. *
    7. 教室表 JSB —— 翻译中文名称(含容纳人数/设施等信息)
    8. *
    *

    */ CLASSROOM_CONFLICT("CLASSROOM_CONFLICT", "教室时间冲突检查", - "同一教室在同一时间段被多门课程占用"), + "同一教室在同一时间段被多门课程占用,或场地历不可用时段已有课程"), /** * 教学保障资源冲突检查(预留/按需开启)。 diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/scheduling/SchedulingViewVO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/scheduling/SchedulingViewVO.java index d0d431f1..e9e58688 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/scheduling/SchedulingViewVO.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/scheduling/SchedulingViewVO.java @@ -34,11 +34,26 @@ public class SchedulingViewVO { /** 时间区:按周切分,格子叠加校历/班历不可排与已排课次 */ private List weeks = new ArrayList<>(); + /** 节次字典(节次时间表),供前端显示控制(7-8 节 / 晚上 / 夜间) */ + private List periods = new ArrayList<>(); + + @Data + public static class Period { + /** 节次号 */ + private Integer jc; + /** 节次名称/简称 */ + private String label; + /** 时段:上午 / 下午 / 晚上 */ + private String sd; + } + @Data public static class Course { private String sskcbh; private String kbh; private String kcmc; + /** 课程简称(任务表 简称,格内优先显示) */ + private String jc; private String klx; private Integer xs; private Integer zks; @@ -46,12 +61,27 @@ public class SchedulingViewVO { private String jyxm; private String jsbh; private String jsmc; + /** 排课建议(教研室任务书填报的 教研室计划备注) */ + private String jysjhbz; + /** 教研室计划教员编号 / 姓名 */ + private String jysjhjybh; + private String jysjhjyxm; /** 已排学时(每节 2 学时 + 节次调节) */ private Integer scheduledHours; /** 是否已排满(scheduledHours >= xs) */ private Boolean full; /** 当前用户是否可编辑该课程 */ private Boolean editable; + /** 合班班次(⑤ 合班信息) */ + private List teams = new ArrayList<>(); + } + + /** 班次学期(学员队 + 学期)简项 */ + @Data + public static class Team { + private String xydxqbh; + private String xydbh; + private String xydmc; } @Data @@ -78,8 +108,16 @@ public class SchedulingViewVO { private Boolean unavailable; /** 硬冲突原因(不可排课来源:校历/班历等) */ private String reason; + /** 不可排来源类别:SCHOOL 校历 / CLASS 班历 */ + private String calendarKind; /** 软提示(非正课 / 配当周次不符) */ private String warning; + /** 是否为非正课时段 */ + private Boolean soft; + /** 该节次被场地历标记不可用的教室编号 */ + private List blockedRooms = new ArrayList<>(); + /** 该节次被教员历标记不可用的教员编号 */ + private List blockedTeachers = new ArrayList<>(); private List lessons = new ArrayList<>(); } @@ -89,10 +127,21 @@ public class SchedulingViewVO { private String sskcbh; private String kcmc; private String jxnr; + private String jxyd; private String jxff; private Integer jcdj; private String jyxm; + private String jybh; private String jsmc; + private String jsbh; + private String jxbzbz; + private String bzms; private String ycxx; + /** 本次课次的学员队(教学班次) */ + private List xydbhs = new ArrayList<>(); + /** 本次课次的教室(可多场地) */ + private List jsbhs = new ArrayList<>(); + /** 本次课次的授课教员(含主讲/辅讲) */ + private List jybhs = new ArrayList<>(); } } diff --git a/frontend/src/api/teachBusiness/schedulingWindow.js b/frontend/src/api/teachBusiness/schedulingWindow.js index d527c800..766f250e 100644 --- a/frontend/src/api/teachBusiness/schedulingWindow.js +++ b/frontend/src/api/teachBusiness/schedulingWindow.js @@ -58,3 +58,47 @@ export function listSchedulingLogs(sskcbh, czlx) { params: { sskcbh, czlx } }) } + +// 编辑课程教学任务信息(学时/周课时/课程简称/责任教员/默认场地) +export function updateSchedulingCourse(data) { + return request({ + url: '/schedulingWindow/course/update', + method: 'post', + data + }) +} + +// 教学班次(合班)当前列表 + 候选班次 +export function getCourseTeams(sskcbh) { + return request({ + url: '/schedulingWindow/course/teams', + method: 'get', + params: { sskcbh } + }) +} + +// 教学保障选项(保障类别 + 保障资源,含总量与单位) +export function getSupportOptions() { + return request({ + url: '/schedulingWindow/support/options', + method: 'get' + }) +} + +// 批量发布【运行课表】(手册 12.1) +export function publishRunningCourses(xydxqbhs) { + return request({ + url: '/schedulingWindow/publish', + method: 'post', + data: { xydxqbhs } + }) +} + +// 批量删除【运行课表】(手册 12.2) +export function withdrawRunningCourses(xydxqbhs) { + return request({ + url: '/schedulingWindow/withdraw', + method: 'post', + data: { xydxqbhs } + }) +} diff --git a/frontend/src/api/teachOffice/teacherCalendar.js b/frontend/src/api/teachOffice/teacherCalendar.js new file mode 100644 index 00000000..e4c46920 --- /dev/null +++ b/frontend/src/api/teachOffice/teacherCalendar.js @@ -0,0 +1,32 @@ +import request from '@/utils/request' + +// 教员历(后端 /teacherCalendar/jyl)。 +// 教员历 = 教员自己的工作时间表:标出的时段「不可排课」。 +// 排课窗口的硬冲突「教员历不可用」与批量冲突检查的教员时间冲突都读这张表。 + +// 按教员编号查询该教员已登记的教员历(返回全部,前端按周过滤) +export function listTeacherCalendarByJybh(jybh) { + return request({ + url: '/teacherCalendar/jyl/listByJybh', + method: 'get', + params: { jybh } + }) +} + +// 批量保存(按 教员编号+日期+节次 幂等覆盖) +export function batchSaveTeacherCalendar(list) { + return request({ + url: '/teacherCalendar/jyl/batchSave', + method: 'post', + data: list + }) +} + +// 批量删除(教员历表无 del_flag 审计列,为物理删除) +export function batchDeleteTeacherCalendar(bhs) { + return request({ + url: '/teacherCalendar/jyl/batchDelete', + method: 'post', + data: bhs + }) +} diff --git a/frontend/src/views/teachBusiness/schedulingWindow/index.vue b/frontend/src/views/teachBusiness/schedulingWindow/index.vue index 36ccdc85..eff66a32 100644 --- a/frontend/src/views/teachBusiness/schedulingWindow/index.vue +++ b/frontend/src/views/teachBusiness/schedulingWindow/index.vue @@ -63,6 +63,7 @@ >
    {{ c.kcmc || '未命名课程' }} + {{ c.jc }} 已排满 待排 {{ remainHours(c) }} 只读 @@ -71,6 +72,14 @@ 教员:{{ c.jyxm || '—' }} 场地:{{ c.jsmc || c.jsbh || '未设置' }}
    +
    + 合班 {{ c.teams.length }} 个班次:{{ c.teams.map(t => t.xydmc).join('、') }} + 班次:{{ (c.teams && c.teams[0] && c.teams[0].xydmc) || view.xydmc || '—' }} +
    +
    + 教研室计划教员:{{ c.jysjhjyxm }} + 排课建议:{{ c.jysjhbz }} +
    {{ c.xs ? (c.scheduledHours || 0) + ' / ' + c.xs + ' 学时' : '学时未设置' }}
    -
    课程编号 {{ shortId(c.sskcbh) }}
    +
    + 课程编号 {{ shortId(c.sskcbh) }} + 编辑课程信息 +
    @@ -91,68 +103,161 @@
    - 上一周 - - 第 {{ currentWeek ? currentWeek.weekNo : '—' }} 周 - {{ fmtDate(currentWeek.startDate) }} ~ {{ fmtDate(currentWeek.endDate) }} - - 下一周 + + 单周视图 + 全学期视图 + + + 全学期 {{ weeks.length }} 周(可直接跨周框选)
    可排 已选 已排课次 + 当前课程 软提示 + 冲突 不可排
    +
    + 冲突显示 + + {{ d.label }} + + 显示节次 + + 7-8 节 + 晚上 + 夜间 + + 重置 +
    -
    -
    -
    -
    节次
    -
    -
    {{ weekdayName(d.weekday) }}
    -
    {{ fmtDate(d.date) }}
    +
    +
    +
    该班次学期暂无可用节次
    + + +
    +
    +
    当前格子编排信息
    +
    点选格子查看详情;双击已排格子可回填预排信息
    +
    +
    + 日期节次 + {{ focusedCell.rq }} 第 {{ focusedCell.jc }} 节 +
    +
    + 冲突 + {{ focusedCellCell.reason }} +
    +
    + 提示 + {{ focusedCellCell.warning }} +
    +
    + 已排课次 + +
    + {{ l.kcmc }} + |{{ l.jxnr }} + |{{ l.jxff }} + |{{ l.jyxm }} + |{{ l.jsmc }} + |节次调节 {{ l.jcdj > 0 ? '+' : '' }}{{ l.jcdj }} + |班次 {{ l.xydbhs.length }} 个 + |保障 {{ l.jxbzbz }} + |用车 {{ l.ycxx }} +
    +
    +
    +
    该格暂未排课
    +
    +
    +
    +
    选择区域统计
    +
    框选节次后显示统计
    +
    +
    格数{{ selectedCells.length }} 格,合计 {{ selectedHours }} 学时
    +
    日期范围{{ statsRangeText }}
    +
    + 按周分布 + {{ statsWeekText }} +
    +
    + 当前课程占 + {{ pickedLessonCount }} 个课次({{ currentCourse ? (currentCourse.kcmc || '') : '未选课程' }}) +
    +
    + 其它课程 + {{ statsOthers.join(';') }} +
    +
    +
    +
    @@ -167,6 +272,7 @@ 已选 {{ selectedCells.length }}清除选择 + 快捷键:S 安排 · R 删除(不在输入框时可加 Alt)· Ctrl+框选多区域 · 双击回填
    删除所选节次 + 编辑课程信息 - +
    课程:{{ currentCourse ? currentCourse.kcmc : '—' }} 节次:{{ selectedCells.length }} 范围:{{ selectedRangeText }}
    - - - +
    + 排课建议:{{ currentCourse.jysjhbz }} +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    默认使用该课程的全部合班班次;实践教学拆分时移除其它班次,需要集中上课时可加入其它班次。
    +
    +
    + {{ t.xydmc || t.xydbh }} + {{ t.xydbh }} + 移除 +
    +
    至少保留一个教学班次
    +
    +
    + + + + 增加教学班次 +
    +
    + + + + + + + + +
    + + 增加辅讲教员 +
    +
    +
    + - {{ f }} + 移除 +
    +
    +
    +
    +
    + + + +
    默认使用课程默认场地;可加入多个场地(如理论教室 + 实践场地)。
    +
    +
    + {{ roomLabel(r) }} + {{ r }} + 移除 +
    +
    未指定时使用课程默认场地(未设置则会排课失败)
    +
    +
    + + + + 增加教学场地 +
    +
    + + + +
    + 保障资源尚未登记主数据,直接手动录入保障需求文字,保存到「教学保障备注」。 +
    + + + + + +
    + + + + +
    +
    +
    +
    +
    + +
    + 取消 + 确认排课 +
    +
    + + + + + + - - + + - - - - + + - - - - - - + + - - + + + + + - - - - - - - - -
    - 取消 - 确认排课 + 取消 + 保存
    @@ -287,7 +523,9 @@ import { deleteLessonCells, clearCourseLessons, deleteRunningCourse, - listSchedulingLogs + listSchedulingLogs, + updateSchedulingCourse, + getCourseTeams } from '@/api/teachBusiness/schedulingWindow' import { listSemester } from '@/api/studentRecords/semester' import { listAllClassroom } from '@/api/teachBusiness/classroom' @@ -304,11 +542,31 @@ export default { loading: false, submitting: false, currentCourse: null, + /** 已选格子 [{rq, jc}] */ selectedCells: [], dragStart: null, dragEnd: null, dragging: false, + /** 本次拖拽是否为追加(Ctrl / Meta) */ + additiveDrag: false, + /** 当前聚焦格(⑨ 当前格子编排信息) */ + focusedCell: null, + /** 视图模式:week 单周 / all 全学期 */ + viewMode: 'week', + /** 冲突显示选项(按维度着色) */ + conflictDims: [], + conflictDimOptions: [ + { key: 'ROOM', label: '教室' }, + { key: 'TEACHER', label: '教员' }, + { key: 'TEAM', label: '班次' }, + { key: 'ROOM_CAL', label: '场地历' }, + { key: 'TEACHER_CAL', label: '教员历' }, + { key: 'CALENDAR', label: '校历/班历' } + ], + /** 节次显示控制(④ 显示控制选项) */ + periodToggles: ['p78', 'eve', 'night'], formVisible: false, + formTab: 'subject', logsVisible: false, logs: [], jxffOptions: ['理论讲授', '实践操作', '案例研讨', '综合演练', '现地教学', '自学'], @@ -320,8 +578,25 @@ export default { jxbzbz: '', ycxx: '', jsbh: '', - jybh: '' - } + jybh: '', + jsbhs: [], + fzjybhs: [], + xydbhs: [] + }, + /** 教学班次面板 */ + arrangeTeams: [], + candidateTeams: [], + teamToAdd: '', + fzjyToAdd: '', + roomToAdd: '', + /** 用车信息四要素 */ + carDepartTime: '', + carDepartPlace: '', + carReturnTime: '', + carDest: '', + /** 编辑课程教学任务信息 */ + courseFormVisible: false, + courseForm: { sskcbh: '', kcmc: '', jc: '', xs: 0, zks: 0, jybh: '', jsbh: '' } } }, computed: { @@ -337,12 +612,48 @@ export default { days() { return this.currentWeek && this.currentWeek.days ? this.currentWeek.days : [] }, - jcList() { - const set = new Set() - this.days.forEach(d => { - (d.cells || []).forEach(c => set.add(c.jc)) + /** 全学期扁平日列表(跨周框选用) */ + flatDays() { + const list = [] + const index = {} + this.weeks.forEach(w => { + (w.days || []).forEach(d => { + index[this.fmtDate(d.date)] = list.length + list.push(d) + }) }) - return Array.from(set).sort((a, b) => a - b) + return { list, index } + }, + /** 网格行:单周一行 / 全学期每周一组 */ + gridRows() { + if (this.viewMode === 'all') { + return this.weeks.map(w => ({ + key: 'w' + w.weekNo, + weekNo: w.weekNo, + days: (w.days || []).map(d => Object.assign({}, d, { fi: this.fiOf(d.date) })) + })) + } + const w = this.currentWeek + if (!w) return [] + return [{ + key: 'w' + w.weekNo, + weekNo: w.weekNo, + days: (w.days || []).map(d => Object.assign({}, d, { fi: this.fiOf(d.date) })) + }] + }, + /** 节次字典(来自节次时间表),缺失时按数据推导 */ + periodDict() { + const list = (this.view && this.view.periods) || [] + if (list.length) return list + const set = new Set() + this.weeks.forEach(w => (w.days || []).forEach(d => (d.cells || []).forEach(c => set.add(c.jc)))) + return Array.from(set).sort((a, b) => a - b).map(jc => ({ jc, label: String(jc), sd: jc <= 4 ? '上午' : '下午' })) + }, + /** 应用显示控制后的节次行 */ + jcList() { + return this.periodDict + .filter(p => this.periodVisible(p)) + .map(p => p.jc) }, canArrange() { return !!(this.currentCourse && this.currentCourse.editable && this.selectedCells.length) @@ -350,24 +661,65 @@ export default { canOperate() { return !!(this.currentCourse && this.currentCourse.editable) }, - // 所选格中属于当前课程的已排课次数 + /** 所选格中属于当前课程的已排课次数 */ pickedLessonCount() { let n = 0 - this.days.forEach(d => { - (d.cells || []).forEach(c => { - if (this.isSelected(d.date, c.jc) && this.currentCourse) { - n += (c.lessons || []).filter(l => l.sskcbh === this.currentCourse.sskcbh).length - } - }) + this.eachCell((day, cell) => { + if (this.isSelected(day.date, cell.jc) && this.currentCourse) { + n += (cell.lessons || []).filter(l => l.sskcbh === this.currentCourse.sskcbh).length + } }) return n }, + /** 选区统计:合计学时(每节 2 学时) */ + selectedHours() { + return this.selectedCells.length * 2 + }, selectedRangeText() { if (!this.selectedCells.length) return '' const dates = Array.from(new Set(this.selectedCells.map(c => c.rq))).sort() const jcs = Array.from(new Set(this.selectedCells.map(c => c.jc))).sort((a, b) => a - b) const dateText = dates.length > 2 ? `${dates[0]} ~ ${dates[dates.length - 1]}(${dates.length} 天)` : dates.join('、') return `${dateText} 第 ${jcs.join('、')} 节` + }, + statsRangeText() { + return this.selectedRangeText || '—' + }, + /** 选区按周分布 */ + statsWeekText() { + const counter = {} + this.selectedCells.forEach(c => { + const w = this.weekNoOfDate(c.rq) + counter[w] = (counter[w] || 0) + 1 + }) + const keys = Object.keys(counter).map(Number).sort((a, b) => a - b) + if (!keys.length) return '—' + return keys.map(w => `第 ${w} 周 ${counter[w]} 格`).join('、') + }, + /** 选区内其它课程的课次 */ + statsOthers() { + const map = {} + this.eachCell((day, cell) => { + if (!this.isSelected(day.date, cell.jc)) return + ;(cell.lessons || []).forEach(l => { + if (this.currentCourse && l.sskcbh === this.currentCourse.sskcbh) return + map[l.kcmc || '未知课程'] = (map[l.kcmc || '未知课程'] || 0) + 1 + }) + }) + return Object.keys(map).map(k => `${k} ${map[k]} 次`) + }, + focusedCellCell() { + if (!this.focusedCell) return null + return this.cellByKey(this.focusedCell.rq, this.focusedCell.jc) + }, + focusedLessons() { + const cell = this.focusedCellCell + return cell && cell.lessons ? cell.lessons : [] + }, + /** 可加入的教学班次 */ + addableTeams() { + const used = this.arrangeTeams.map(t => t.xydbh) + return this.candidateTeams.filter(t => !used.includes(t.xydbh)) } }, created() { @@ -378,6 +730,12 @@ export default { this.loadClassrooms() if (this.xydxqbh) this.loadView() }, + mounted() { + window.addEventListener('keydown', this.onGlobalKeydown) + }, + beforeDestroy() { + window.removeEventListener('keydown', this.onGlobalKeydown) + }, methods: { /* ==================== 数据加载 ==================== */ loadSemesters() { @@ -400,6 +758,7 @@ export default { this.view = {} this.currentCourse = null this.selectedCells = [] + this.focusedCell = null return } this.loading = true @@ -407,6 +766,10 @@ export default { getSchedulingView(this.xydxqbh).then(res => { this.view = (res && res.data) || {} this.selectedCells = [] + this.focusedCell = null + this.dragging = false + this.dragStart = null + this.dragEnd = null // 尽量保留原来选中的课程 const list = this.courses this.currentCourse = keepBh ? (list.find(c => c.sskcbh === keepBh) || list[0] || null) : (list[0] || null) @@ -417,6 +780,7 @@ export default { this.view = {} this.currentCourse = null this.selectedCells = [] + this.focusedCell = null this.$message.warning('排课视图加载失败:该班次学期可能暂无课程数据,或课程/场地数据不完整') if (window.console && console.error) console.error(err) }) @@ -469,6 +833,75 @@ export default { cellKey(rq, jc) { return `${this.fmtDate(rq)}#${jc}` }, + /** 全学期扁平序号(跨周框选统一寻址) */ + fiOf(date) { + const key = this.fmtDate(date) + const idx = this.flatDays.index[key] + return idx === undefined ? -1 : idx + }, + eachCell(fn) { + this.weeks.forEach(w => (w.days || []).forEach(d => (d.cells || []).forEach(c => fn(d, c)))) + }, + cellByKey(rq, jc) { + const key = this.fmtDate(rq) + const idx = this.flatDays.index[key] + const day = idx === undefined ? null : this.flatDays.list[idx] + return day ? this.cellOf(day, jc) : null + }, + weekNoOfDate(rq) { + const key = this.fmtDate(rq) + const hit = this.weeks.find(w => (w.days || []).some(d => this.fmtDate(d.date) === key)) + return hit ? hit.weekNo : 0 + }, + /** ④ 显示控制选项:7-8 节 / 晚上 / 夜间 */ + periodVisible(p) { + const jc = p.jc + if (jc <= 6) return true + if (jc === 7 || jc === 8) return this.periodToggles.includes('p78') + if (jc <= 10) return this.periodToggles.includes((p.sd || '') === '晚上' ? 'eve' : 'night') + return this.periodToggles.includes('night') + }, + periodLabel(jc) { + const p = this.periodDict.find(x => x.jc === jc) + return p ? `${p.label || jc}(${p.sd || ''})` : String(jc) + }, + /** 格内显示课程简称(无简称退化为课程名) */ + shortName(l) { + if (!l) return '课程' + const c = this.courses.find(x => x.sskcbh === l.sskcbh) + return (c && c.jc) || l.kcmc || l.jxnr || '课程' + }, + roomLabel(jsbh) { + if (!jsbh) return '' + const r = this.classroomOptions.find(x => x.jsbh === jsbh) + return r ? (r.jsmc ? r.jsbh + '(' + r.jsmc + ')' : r.jsbh) : jsbh + }, + /** ② 冲突显示选项:命中当前课程预排条件的冲突维度 */ + conflictKinds(day, jc) { + const kinds = [] + const cell = this.cellOf(day, jc) + if (!cell) return kinds + if (cell.calendarKind || cell.reason) kinds.push('CALENDAR') + const room = this.arrangeForm.jsbhs[0] || (this.currentCourse && this.currentCourse.jsbh) + const teacher = this.arrangeForm.jybh || (this.currentCourse && this.currentCourse.jybh) + const myCourse = this.currentCourse ? this.currentCourse.sskcbh : null + const mineTeams = this.currentCourse && this.currentCourse.teams + ? this.currentCourse.teams.map(t => t.xydbh) : [] + if (room && (cell.blockedRooms || []).includes(room)) kinds.push('ROOM_CAL') + if (teacher && (cell.blockedTeachers || []).includes(teacher)) kinds.push('TEACHER_CAL') + ;(cell.lessons || []).forEach(l => { + if (l.sskcbh === myCourse) return + if (room && (l.jsbhs || []).includes(room)) kinds.push('ROOM') + if (teacher && (l.jybhs || []).includes(teacher)) kinds.push('TEACHER') + if ((l.xydbhs || []).some(t => mineTeams.includes(t))) kinds.push('TEAM') + }) + return Array.from(new Set(kinds)) + }, + /** 按所选维度命中的冲突 */ + conflictHit(day, jc) { + if (!this.conflictDims.length) return false + return this.conflictKinds(day, jc).some(k => this.conflictDims.includes(k)) + }, isSelected(rq, jc) { return this.selectedCells.some(c => c.rq === this.fmtDate(rq) && c.jc === jc) }, @@ -480,6 +913,7 @@ export default { 'is-warn': !!(cell && !cell.unavailable && cell.warning), 'is-picked': !hasLesson && this.isSelected(day.date, jc), 'has-lesson': hasLesson, + 'is-conflict': !!(cell && !cell.unavailable && this.conflictHit(day, jc)), 'day-off': !!day.unavailableReason } }, @@ -510,33 +944,40 @@ export default { this.$message.info(`«${c.kcmc || ''}» 当前账号无排课权限,仅可查看`) } }, - startDrag(dayIdx, jc, day) { + startDrag(fi, jc, day, ev) { const cell = this.cellOf(day, jc) if (cell && cell.unavailable) { this.$message.warning(cell.reason || '该节次不可排课') return } this.dragging = true - this.dragStart = { dayIdx, jc } - this.dragEnd = { dayIdx, jc } + this.additiveDrag = !!(ev && (ev.ctrlKey || ev.metaKey)) + this.dragStart = { fi, jc } + this.dragEnd = { fi, jc } + this.focusCell(day.date, jc) + if (this.$refs.gridWrap && this.$refs.gridWrap.focus) this.$refs.gridWrap.focus() }, - hoverCell(dayIdx, jc) { + hoverCell(fi, jc) { if (!this.dragging) return - this.dragEnd = { dayIdx, jc } + this.dragEnd = { fi, jc } }, finishDrag() { if (!this.dragging) return this.dragging = false const s = this.dragStart const e = this.dragEnd + const additive = this.additiveDrag + this.dragStart = null + this.dragEnd = null + this.additiveDrag = false if (!s || !e) return - const dayFrom = Math.min(s.dayIdx, e.dayIdx) - const dayTo = Math.max(s.dayIdx, e.dayIdx) + const fiFrom = Math.min(s.fi, e.fi) + const fiTo = Math.max(s.fi, e.fi) const jcFrom = Math.min(s.jc, e.jc) const jcTo = Math.max(s.jc, e.jc) const picked = [] - for (let di = dayFrom; di <= dayTo; di++) { - const day = this.days[di] + for (let fi = fiFrom; fi <= fiTo; fi++) { + const day = this.flatDays.list[fi] if (!day || day.unavailableReason) continue for (let jc = jcFrom; jc <= jcTo; jc++) { const cell = this.cellOf(day, jc) @@ -544,26 +985,90 @@ export default { picked.push({ rq: this.fmtDate(day.date), jc }) } } - // 单选格:点一下即选中/取消 - if (picked.length === 1) { - const one = picked[0] - const idx = this.selectedCells.findIndex(c => c.rq === one.rq && c.jc === one.jc) - if (idx >= 0) { - this.selectedCells.splice(idx, 1) - } else { - this.selectedCells.push(one) - } - } else { - const map = new Map() - this.selectedCells.concat(picked).forEach(c => map.set(`${c.rq}#${c.jc}`, c)) - this.selectedCells = Array.from(map.values()) + const map = new Map() + if (additive) { + this.selectedCells.forEach(c => map.set(this.cellKey(c.rq, c.jc), c)) } - this.dragStart = null - this.dragEnd = null + // 单格且非追加:点一下选中/取消 + if (picked.length === 1 && !additive) { + const one = picked[0] + if (this.isSelected(one.rq, one.jc)) { + this.selectedCells = [] + return + } + this.selectedCells = [one] + return + } + picked.forEach(c => map.set(this.cellKey(c.rq, c.jc), c)) + this.selectedCells = Array.from(map.values()) + .sort((a, b) => (a.rq === b.rq ? a.jc - b.jc : a.rq < b.rq ? -1 : 1)) }, clearSelection() { this.selectedCells = [] }, + /** ⑨ 当前格子编排信息 */ + focusCell(rq, jc) { + this.focusedCell = { rq: this.fmtDate(rq), jc } + }, + /** 双击格子:反选课程 + 回填预排信息(手册 12.3.2) */ + dblclickCell(day, jc) { + const cell = this.cellOf(day, jc) + if (cell && cell.unavailable) { + this.$message.warning(cell.reason || '该节次不可排课') + return + } + this.focusCell(day.date, jc) + const lessons = (cell && cell.lessons) || [] + if (!lessons.length) return + const first = lessons[0] + const course = this.courses.find(c => c.sskcbh === first.sskcbh) + if (course) { + this.currentCourse = course + } + // 回填:教学方法 / 教学内容 / 教学要点 / 节次调节 / 场地 / 教员 / 教学班次 + this.arrangeForm.jxnr = first.jxnr || '' + this.arrangeForm.jxyd = first.jxyd || '' + this.arrangeForm.jxff = first.jxff || '理论讲授' + this.arrangeForm.jcdj = first.jcdj || 0 + this.arrangeForm.jxbzbz = first.jxbzbz || '' + this.arrangeForm.ycxx = first.ycxx || '' + this.arrangeForm.jsbhs = (first.jsbhs || []).slice() + this.arrangeForm.jsbh = this.arrangeForm.jsbhs[0] || '' + this.arrangeForm.jybh = first.jybh || '' + this.arrangeForm.xydbhs = (first.xydbhs || []).slice() + this.arrangeTeams = (first.xydbhs || []).map(bh => ({ xydbh: bh, xydmc: this.teamNameOf(bh) })) + this.$message.success(`已回填「${first.kcmc || ''}」的预排信息`) + }, + teamNameOf(xydbh) { + const t = (this.currentCourse && this.currentCourse.teams || []).find(x => x.xydbh === xydbh) + if (t) return t.xydmc + const c = this.candidateTeams.find(x => x.xydbh === xydbh) + return c ? c.xydmc : xydbh + }, + /** 快捷键:S 安排 / R 删除(手册 12.3.4 / 12.3.5) */ + onKeydown(ev) { + const tag = (ev.target && ev.target.tagName) || '' + const typing = ['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || (ev.target && ev.target.isContentEditable) + if (typing) return + this.handleHotkey(ev) + }, + onGlobalKeydown(ev) { + const tag = (ev.target && ev.target.tagName) || '' + const typing = ['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || (ev.target && ev.target.isContentEditable) + if (typing || ev.ctrlKey || ev.metaKey) return + // 时间区未获得焦点时使用 Alt+S / Alt+R + if (ev.altKey) this.handleHotkey(ev) + }, + handleHotkey(ev) { + const key = (ev.key || '').toLowerCase() + if (key === 's') { + ev.preventDefault() + if (this.canArrange) this.openArrangeForm() + } else if (key === 'r') { + ev.preventDefault() + if (this.canOperate && this.pickedLessonCount) this.handleDeleteCells() + } + }, /* ==================== 排课 ==================== */ openArrangeForm() { @@ -571,32 +1076,112 @@ export default { this.$message.warning('请先选择课程,并在右侧格子中选择要安排的节次') return } + this.formTab = 'subject' this.arrangeForm.jxnr = '' this.arrangeForm.jxyd = '' this.arrangeForm.jxff = '理论讲授' this.arrangeForm.jcdj = 0 this.arrangeForm.jxbzbz = '' this.arrangeForm.ycxx = '' - this.arrangeForm.jsbh = '' this.arrangeForm.jybh = '' + this.arrangeForm.fzjybhs = [] + // 场地默认课程默认场地;教学班次默认课程全部合班班次 + this.arrangeForm.jsbhs = this.currentCourse.jsbh ? [this.currentCourse.jsbh] : [] + this.arrangeForm.jsbh = this.arrangeForm.jsbhs[0] || '' + this.carDepartTime = '' + this.carDepartPlace = '' + this.carReturnTime = '' + this.carDest = '' + this.fzjyToAdd = '' + this.roomToAdd = '' + this.teamToAdd = '' + this.loadCourseTeams() this.formVisible = true }, + /** 教学班次:当前 + 候选 */ + loadCourseTeams() { + const course = this.currentCourse + this.arrangeTeams = (course.teams || []).map(t => ({ xydbh: t.xydbh, xydmc: t.xydmc, rs: null })) + this.candidateTeams = [] + if (!course || !course.sskcbh) return + getCourseTeams(course.sskcbh).then(res => { + const data = (res && res.data) || {} + if (data.current && data.current.length) this.arrangeTeams = data.current + this.candidateTeams = data.candidates || [] + }).catch(() => {}) + }, + addTeam() { + if (!this.teamToAdd) { + this.$message.warning('请选择要加入的教学班次') + return + } + const t = this.candidateTeams.find(x => x.xydbh === this.teamToAdd) + this.arrangeTeams.push({ xydbh: this.teamToAdd, xydmc: t ? t.xydmc : this.teamToAdd, rs: t ? t.rs : null }) + this.teamToAdd = '' + }, + removeTeam(t) { + if (this.arrangeTeams.length <= 1) { + this.$message.warning('至少保留一个教学班次') + return + } + const idx = this.arrangeTeams.indexOf(t) + if (idx >= 0) this.arrangeTeams.splice(idx, 1) + }, + addFzjy() { + const bh = (this.fzjyToAdd || '').trim() + if (!bh) { + this.$message.warning('请输入辅讲教员工号') + return + } + if (!this.arrangeForm.fzjybhs.includes(bh)) this.arrangeForm.fzjybhs.push(bh) + this.fzjyToAdd = '' + }, + addRoom() { + const bh = (this.roomToAdd || '').trim() + if (!bh) { + this.$message.warning('请选择或输入教学场地') + return + } + if (!this.arrangeForm.jsbhs.includes(bh)) this.arrangeForm.jsbhs.push(bh) + this.arrangeForm.jsbh = this.arrangeForm.jsbhs[0] + this.roomToAdd = '' + }, + /** 用车信息四要素合成文本(字段 ycxx 存文本) */ + composeCar() { + const parts = [] + if (this.carDepartTime) parts.push('出发 ' + this.carDepartTime) + if (this.carDepartPlace) parts.push('自 ' + this.carDepartPlace) + if (this.carReturnTime) parts.push('返回 ' + this.carReturnTime) + if (this.carDest) parts.push('目的地 ' + this.carDest) + return parts.join(';') + }, submitArrange() { if (!this.selectedCells.length) { this.$message.warning('未选择节次') return } + if (!this.arrangeTeams.length) { + this.$message.warning('请至少保留一个教学班次') + return + } + if (!this.arrangeForm.jsbhs.length && !(this.currentCourse && this.currentCourse.jsbh)) { + this.$message.warning('课程未设置默认场地,请先指定教学场地') + return + } this.submitting = true const payload = Object.assign({}, this.arrangeForm, { xydxqbh: this.xydxqbh, sskcbh: this.currentCourse.sskcbh, + ycxx: this.composeCar(), + jsbh: this.arrangeForm.jsbhs[0] || '', + xydbhs: this.arrangeTeams.map(t => t.xydbh), cells: this.selectedCells.slice() }) arrangeLessons(payload).then(res => { this.submitting = false this.formVisible = false const data = (res && res.data) || {} - const arranged = (data.arranged || []).length + const arranged = data.arranged || 0 const ignored = data.ignored || [] const warnings = data.warnings || [] this.$message.success(`已排入 ${arranged} 个节次`) @@ -665,8 +1250,49 @@ export default { this.logs = [] this.logsVisible = true }) - } - } + }, + + /* ==================== 编辑课程教学任务信息(手册 12.3.1) ==================== */ + openCourseEdit(course) { + const c = course || this.currentCourse + if (!c) { + this.$message.warning('请先选择课程') + return + } + if (!c.editable) { + this.$message.warning('当前账号无该课程的信息修改权限') + return + } + this.courseForm = { + sskcbh: c.sskcbh, + kcmc: c.kcmc, + jc: c.jc || '', + xs: c.xs || 0, + zks: c.zks || 0, + jybh: c.jybh || '', + jsbh: c.jsbh || '' + } + this.courseFormVisible = true + }, + submitCourseEdit() { + if (!this.courseForm.sskcbh) return + this.submitting = true + updateSchedulingCourse({ + sskcbh: this.courseForm.sskcbh, + xs: this.courseForm.xs, + zks: this.courseForm.zks, + jc: this.courseForm.jc, + jybh: this.courseForm.jybh, + jsbh: this.courseForm.jsbh + }).then(() => { + this.submitting = false + this.courseFormVisible = false + this.$message.success('课程信息已保存') + this.loadView() + }).catch(() => { + this.submitting = false + }) + } } } @@ -1058,3 +1684,208 @@ export default { margin-left: 8px; } + + + diff --git a/frontend/src/views/teachBusiness/timetableConflict/index.vue b/frontend/src/views/teachBusiness/timetableConflict/index.vue index 2649f953..98f77b29 100644 --- a/frontend/src/views/teachBusiness/timetableConflict/index.vue +++ b/frontend/src/views/teachBusiness/timetableConflict/index.vue @@ -99,11 +99,12 @@ import { listAllSemester } from '@/api/teachBusiness/semester' // 四类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量; // 检查状态与冲突数完全来自后端接口) +// 注意:教员卡片含「教员历不可排」、教室卡片含「场地历不可用」,与排课窗口硬冲突口径一致 const CARD_DEFS = [ { key: 'TEAM_CONFLICT', label: '教学班时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程' }, { key: 'ELECTIVE_REQUIRED_CONFLICT', label: '教学班必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠' }, - { key: 'TEACHER_CONFLICT', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程' }, - { key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用' } + { key: 'TEACHER_CONFLICT', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程,或教员历不可排课时段已有课程' }, + { key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用,或场地历不可用时段已有课程' } ] export default { diff --git a/frontend/src/views/teachOffice/shiftSemester/index.vue b/frontend/src/views/teachOffice/shiftSemester/index.vue index 449c184f..1875b52f 100644 --- a/frontend/src/views/teachOffice/shiftSemester/index.vue +++ b/frontend/src/views/teachOffice/shiftSemester/index.vue @@ -64,6 +64,8 @@ 编辑班历 编辑班次教学任务 教学配当 + 发布运行课表 + 删除运行课表
    @@ -333,6 +335,7 @@ import { batchAutoGenerateTasks } from '@/api/studentRecords/classCalendar' import { listSemester, allSemester, addSemester, updateSemester, delSemester, batchDeleteSemester, batchUpdateDateRange, batchUpdateJxrwbh, batchCreateSemester, batchUpdateXqdc, batchUpdateKfjypk, batchWaveMerge, batchWaveSplit, listCreatableClasses, presetSemesterDates } from '@/api/studentRecords/semester' import { listAllSemester } from '@/api/teachBusiness/semester' import { listTeachingTask } from '@/api/teachBusiness/teachingTask' +import { publishRunningCourses, withdrawRunningCourses } from '@/api/teachBusiness/schedulingWindow' export default { name: 'ShiftSemester', @@ -346,6 +349,8 @@ export default { return { loading: false, saving: false, + /** 运行课表批量发布/删除进行中 */ + runningPublishing: false, // ==================== 查询条件 ==================== searchForm: { xydbh: '', xqdc: undefined }, @@ -663,6 +668,70 @@ export default { this.allocationVisible = true }, + // 批量发布【运行课表】(手册 12.1):把课程任务发布为运行课程,之后才能在排课窗口排课 + handlePublishRunning() { + const rows = this.selection.slice() + if (!rows.length) { + this.$message.warning('请先勾选班次学期') + return + } + const ids = rows.map(item => item.bh || item.xydxqbh).filter(Boolean) + this.$confirm( + `将发布所选 ${ids.length} 个班次学期的课程到运行课表。
    发布前要求:教学任务已发布未结束、每门课程已指定责任教员与默认场地。`, + '批量发布【运行课表】', + { type: 'warning', dangerouslyUseHTMLString: true } + ).then(() => { + this.batchRunningPublish(publishRunningCourses, ids, '发布') + }).catch(() => {}) + }, + + // 批量删除【运行课表】(手册 12.2):撤回运行课程,已排课次一并删除 + handleWithdrawRunning() { + const rows = this.selection.slice() + if (!rows.length) { + this.$message.warning('请先勾选班次学期') + return + } + const ids = rows.map(item => item.bh || item.xydxqbh).filter(Boolean) + this.$confirm( + `将从运行课表删除所选 ${ids.length} 个班次学期的全部课程。
    这些班次的排课信息(已排课次)将一并删除,编制侧任务保留。`, + '批量删除【运行课表】', + { type: 'warning', dangerouslyUseHTMLString: true } + ).then(() => { + this.batchRunningPublish(withdrawRunningCourses, ids, '删除') + }).catch(() => {}) + }, + + // 批量发布/删除公共处理:逐条展示成功与失败原因 + batchRunningPublish(api, ids, action) { + this.runningPublishing = true + api(ids).then(res => { + const list = (res && res.data) || [] + const ok = list.filter(i => i.success) + const fail = list.filter(i => !i.success) + const okText = ok.length + ? `成功 ${ok.length} 个:` + ok.map(i => `${this.semesterNameOf(i.xydxqbh)}(${i.message})`).join(';') + : '' + const failText = fail.length + ? `失败 ${fail.length} 个:` + fail.map(i => `${this.semesterNameOf(i.xydxqbh)}(${i.message})`).join(';') + : '' + const html = [okText, failText].filter(Boolean).join('
    ') + if (fail.length) { + this.$alert(html, `运行课表${action}结果`, { dangerouslyUseHTMLString: true }) + } else { + this.$message.success(`运行课表${action}完成:成功 ${ok.length} 个`) + } + this.loadList() + }).finally(() => { + this.runningPublishing = false + }) + }, + + semesterNameOf(bh) { + const row = this.selection.find(i => (i.bh || i.xydxqbh) === bh) + return row ? (row.xydmc || row.bh) : bh + }, + handleBatchGenerateTasks() { if (!this.selection.length) { this.$message.warning('请先勾选班次学期') diff --git a/frontend/src/views/teachOffice/teacher/components/TeacherCalendarDialog.vue b/frontend/src/views/teachOffice/teacher/components/TeacherCalendarDialog.vue new file mode 100644 index 00000000..a1873d6d --- /dev/null +++ b/frontend/src/views/teachOffice/teacher/components/TeacherCalendarDialog.vue @@ -0,0 +1,461 @@ + + + + + diff --git a/frontend/src/views/teachOffice/teacher/index.vue b/frontend/src/views/teachOffice/teacher/index.vue index 1267833a..a3185c0e 100644 --- a/frontend/src/views/teachOffice/teacher/index.vue +++ b/frontend/src/views/teachOffice/teacher/index.vue @@ -78,10 +78,11 @@ @click="handleCreateAccount(row)">创建账号 - + @@ -700,6 +701,12 @@ + + +
    @@ -723,10 +730,11 @@ import { getDicts } from '@/api/system/dict/data' import { listRole } from '@/api/system/role' import { saveAs } from 'file-saver' import OrgSyncResultDialog from '@/components/OrgSyncResultDialog' +import TeacherCalendarDialog from './components/TeacherCalendarDialog.vue' export default { name: 'Teacher', - components: { OrgSyncResultDialog }, + components: { OrgSyncResultDialog, TeacherCalendarDialog }, data() { return { // 查询条件(仅后端 JYBMapper.xml 支持的字段:jyxm 模糊 / jysdh 等值 / zc 模糊 / jylb 等值) @@ -790,6 +798,10 @@ export default { attributeData: {}, attributeForm: {}, attributeLoading: false, + + // 教员历(标记不可排课时段) + teacherCalendarVisible: false, + teacherCalendarRow: null, attributeSaving: false, attributeJybh: '', // 字典选项 @@ -1086,6 +1098,17 @@ export default { return payload }, + // ==================== 教员历 ==================== + /** 打开该教员的教员历编辑弹窗(标记不可排课时段) */ + handleTeacherCalendar(row) { + if (!row || !row.jybh) { + this.$message.warning('该教员缺少教员工号,无法维护教员历') + return + } + this.teacherCalendarRow = row + this.teacherCalendarVisible = true + }, + // ==================== 编辑 ==================== handleEdit(row) { this.editDialogVisible = true