From 4a768867df620a75c9aea92ef8df94c8677db002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cwuzhen=E2=80=9D?= Date: Tue, 23 Jun 2026 17:15:49 +0800 Subject: [PATCH] =?UTF-8?q?wuzhen=20update=20=E5=AE=BF=E7=AE=A1=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../XgClassTeacherDormViewController.java | 56 ++ .../domain/XgDormHygieneCheckDeduction.java | 40 ++ .../xg/domain/bo/ClassTeacherDormQueryBo.java | 25 + .../bo/XgDormHygieneCheckCompleteBo.java | 6 + .../bo/XgDormHygieneCheckDeductionBo.java | 30 + .../xg/domain/bo/XgViolationDataBo.java | 1 - .../xg/domain/vo/ClassTeacherClassInfoVo.java | 26 + .../domain/vo/ClassTeacherDormRollCallVo.java | 48 ++ .../vo/ClassTeacherDormViolationVo.java | 55 ++ .../domain/vo/DormHygieneCheckExportVo.java | 6 + .../vo/XgDormHygieneCheckDeductionVo.java | 39 ++ .../xg/domain/vo/XgDormHygieneCheckVo.java | 7 + .../vo/XgDormHygieneStudentScoreVo.java | 5 + .../mapper/XgClassTeacherDormViewMapper.java | 27 + .../XgDormHygieneCheckDeductionMapper.java | 16 + .../XgDormHygieneStudentScoreMapper.java | 12 + .../IXgClassTeacherDormViewService.java | 20 + .../IXgDormHygieneCheckDeductionService.java | 16 + .../XgClassTeacherDormViewServiceImpl.java | 75 +++ .../xg/service/impl/XgDormBedServiceImpl.java | 1 + ...gDormHygieneCheckDeductionServiceImpl.java | 57 ++ .../impl/XgDormHygieneCheckServiceImpl.java | 102 +++- .../XgDormHygieneStudentScoreServiceImpl.java | 6 +- .../impl/XgDormRollCallServiceImpl.java | 35 ++ .../XgClassTeacherDormViewMapper.xml | 138 +++++ .../XgDormHygieneCheckDeductionMapper.xml | 25 + .../XgDormHygieneCheckMapper.xml | 2 + .../XgDormHygieneStudentScoreMapper.xml | 31 ++ docs/开发文档/API/API文档总览.md | 1 + docs/开发文档/API/宿舍卫生检查管理API.md | 38 +- .../xgss/wsjc/components/SingleRoomCheck.vue | 517 +++++++++++++----- .../wsjc/components/ViolationRecordDialog.vue | 407 ++++++++++++-- mobile/src/pages/xgss/wsjc/utils/studentId.js | 10 + .../pages/xgss/wsjc/utils/violationScore.js | 7 + mobile/src/pages/xgss/wsjc/wsjc.vue | 17 +- mobile/src/pages/xgss/wsjc/wsjcDetail.vue | 62 ++- web/src/api/xg/xgss/classTeacherDorm/index.ts | 94 ++++ .../dormHygieneCheck/index.ts | 11 + .../dormHygieneCheck/types.ts | 46 ++ .../views/xg/xgss/classTeacherDorm/index.vue | 401 ++++++++++++++ .../DormHygieneCheckDialog.vue | 24 +- .../DormHygieneCheckEditDialog.vue | 378 ++++++++----- .../DormHygieneCheckMainDialog.vue | 25 +- .../DormHygieneCheckSingleDialog.vue | 132 ++--- .../DisciplineDeductionMultiSelect.vue | 231 ++++++++ .../components/ViolationRecordDialog.vue | 244 ++++++++- .../dormHygieneCheck/genderUtils.ts | 58 ++ .../dormHygieneCheck/index.vue | 25 +- .../dormHygieneCheck/studentIdUtils.ts | 10 + .../dormHygieneCheck/violationScoreUtils.ts | 7 + 50 files changed, 3228 insertions(+), 424 deletions(-) create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassTeacherDormViewController.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneCheckDeduction.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/ClassTeacherDormQueryBo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckDeductionBo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherClassInfoVo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherDormRollCallVo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherDormViolationVo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckDeductionVo.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgClassTeacherDormViewMapper.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormHygieneCheckDeductionMapper.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgClassTeacherDormViewService.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormHygieneCheckDeductionService.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassTeacherDormViewServiceImpl.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckDeductionServiceImpl.java create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/classTeacherDorm/XgClassTeacherDormViewMapper.xml create mode 100644 backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckDeductionMapper.xml create mode 100644 mobile/src/pages/xgss/wsjc/utils/studentId.js create mode 100644 mobile/src/pages/xgss/wsjc/utils/violationScore.js create mode 100644 web/src/api/xg/xgss/classTeacherDorm/index.ts create mode 100644 web/src/views/xg/xgss/classTeacherDorm/index.vue create mode 100644 web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/components/DisciplineDeductionMultiSelect.vue create mode 100644 web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/genderUtils.ts create mode 100644 web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/studentIdUtils.ts create mode 100644 web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/violationScoreUtils.ts diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassTeacherDormViewController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassTeacherDormViewController.java new file mode 100644 index 00000000..cebe8019 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassTeacherDormViewController.java @@ -0,0 +1,56 @@ +package org.dromara.xg.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import lombok.RequiredArgsConstructor; +import org.dromara.common.core.domain.R; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.web.core.BaseController; +import org.dromara.xg.domain.bo.ClassTeacherDormQueryBo; +import org.dromara.xg.domain.vo.ClassTeacherClassInfoVo; +import org.dromara.xg.domain.vo.ClassTeacherDormRollCallVo; +import org.dromara.xg.domain.vo.ClassTeacherDormViolationVo; +import org.dromara.xg.service.IXgClassTeacherDormViewService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 班主任宿舍情况查看(违纪 + 点名,只读) + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/classTeacherDorm/classTeacherDorm") +public class XgClassTeacherDormViewController extends BaseController { + + private final IXgClassTeacherDormViewService classTeacherDormViewService; + + /** + * 当前班主任所带班级信息 + */ + @SaCheckPermission("classTeacherDorm:classTeacherDorm:query") + @GetMapping("/myClass") + public R myClass() { + return R.ok(classTeacherDormViewService.getMyClassInfo()); + } + + /** + * 本班宿舍违纪记录(卫生检查 + 宿舍违纪登记) + */ + @SaCheckPermission("classTeacherDorm:classTeacherDorm:query") + @GetMapping("/violation/list") + public TableDataInfo violationList(ClassTeacherDormQueryBo bo, PageQuery pageQuery) { + return classTeacherDormViewService.queryViolationPage(bo, pageQuery); + } + + /** + * 本班宿舍点名明细 + */ + @SaCheckPermission("classTeacherDorm:classTeacherDorm:query") + @GetMapping("/rollCall/list") + public TableDataInfo rollCallList(ClassTeacherDormQueryBo bo, PageQuery pageQuery) { + return classTeacherDormViewService.queryRollCallPage(bo, pageQuery); + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneCheckDeduction.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneCheckDeduction.java new file mode 100644 index 00000000..0ad054e2 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneCheckDeduction.java @@ -0,0 +1,40 @@ +package org.dromara.xg.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.tenant.core.TenantEntity; + +import java.io.Serial; + +/** + * 宿舍卫生检查扣分明细对象 xg_dorm_hygiene_check_deduction + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("xg_dorm_hygiene_check_deduction") +public class XgDormHygieneCheckDeduction extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId(value = "id") + private Long id; + + @TableLogic(value = "0", delval = "1") + private Long deleted; + + private Long hygieneCheckId; + + private Long disciplineTypeId; + + private String disciplineTypeName; + + private Double score; + + private Integer sortOrder; + + private String remark; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/ClassTeacherDormQueryBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/ClassTeacherDormQueryBo.java new file mode 100644 index 00000000..67771c3d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/ClassTeacherDormQueryBo.java @@ -0,0 +1,25 @@ +package org.dromara.xg.domain.bo; + +import lombok.Data; + +/** + * 班主任宿舍情况查询条件 + */ +@Data +public class ClassTeacherDormQueryBo { + + /** 开始日期 yyyy-MM-dd,默认当天 */ + private String beginDate; + + /** 结束日期 yyyy-MM-dd,默认当天 */ + private String endDate; + + /** 学号 */ + private String studentNo; + + /** 学生姓名 */ + private String studentName; + + /** 点名状态(宿舍点名 Tab 筛选,字典 xg_dorm_roll_call_status) */ + private String rollCallStatus; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckCompleteBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckCompleteBo.java index 18d2202f..5597e609 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckCompleteBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckCompleteBo.java @@ -7,6 +7,7 @@ import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; import jakarta.validation.constraints.*; import jakarta.validation.Valid; +import org.dromara.xg.domain.bo.XgDormHygieneCheckDeductionBo; import java.util.List; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; @@ -159,6 +160,11 @@ public class XgDormHygieneCheckCompleteBo { */ private String disciplineTypeName; + /** + * 扣分明细列表 + */ + private List deductionItems; + /** * 学生评分数据列表 */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckDeductionBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckDeductionBo.java new file mode 100644 index 00000000..82bcd5b7 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckDeductionBo.java @@ -0,0 +1,30 @@ +package org.dromara.xg.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.dromara.xg.domain.XgDormHygieneCheckDeduction; + +/** + * 宿舍卫生检查扣分明细业务对象 + */ +@Data +@AutoMapper(target = XgDormHygieneCheckDeduction.class, reverseConvertGenerate = false) +public class XgDormHygieneCheckDeductionBo { + + private Long id; + + private Long hygieneCheckId; + + @NotNull(message = "扣分项类型不能为空") + private Long disciplineTypeId; + + private String disciplineTypeName; + + @NotNull(message = "扣分分值不能为空") + private Double score; + + private Integer sortOrder; + + private String remark; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgViolationDataBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgViolationDataBo.java index 7ab4228b..cff813c1 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgViolationDataBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgViolationDataBo.java @@ -95,7 +95,6 @@ public class XgViolationDataBo { /** * 违纪原因 */ - @NotBlank(message = "违纪原因不能为空") private String disciplineReason; /** diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherClassInfoVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherClassInfoVo.java new file mode 100644 index 00000000..bf155abc --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherClassInfoVo.java @@ -0,0 +1,26 @@ +package org.dromara.xg.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 班主任班级信息 + */ +@Data +public class ClassTeacherClassInfoVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long classId; + + private String className; + + private String classCode; + + private String collegeName; + + private String majorName; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherDormRollCallVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherDormRollCallVo.java new file mode 100644 index 00000000..87f70004 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherDormRollCallVo.java @@ -0,0 +1,48 @@ +package org.dromara.xg.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 班主任查看-宿舍点名明细 + */ +@Data +public class ClassTeacherDormRollCallVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long id; + + private Long studentId; + + private String studentNo; + + private String studentName; + + private String className; + + private String batchNo; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date checkDate; + + private String checkerName; + + private String buildingName; + + private String roomName; + + private String bedCode; + + /** 0在寝 1请假 2不在 3空床位 */ + private String rollCallStatus; + + private String leaveTypeName; + + private String remark; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherDormViolationVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherDormViolationVo.java new file mode 100644 index 00000000..0d498898 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/ClassTeacherDormViolationVo.java @@ -0,0 +1,55 @@ +package org.dromara.xg.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 班主任查看-宿舍违纪记录 + */ +@Data +public class ClassTeacherDormViolationVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long id; + + /** hygiene_check / dorm_violation */ + private String dataSource; + + /** 来源名称 */ + private String dataSourceName; + + private Long studentId; + + private String studentNo; + + private String studentName; + + private String className; + + private String buildingName; + + private String roomName; + + private String violationTypeName; + + private BigDecimal scoreDeduction; + + private String violationLevel; + + @JsonFormat(pattern = "yyyy-MM-dd") + private Date violationDate; + + private String violationReason; + + private String status; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormHygieneCheckExportVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormHygieneCheckExportVo.java index 2755f48c..7650ecfe 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormHygieneCheckExportVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormHygieneCheckExportVo.java @@ -90,6 +90,12 @@ public class DormHygieneCheckExportVo implements Serializable { @ExcelProperty(value = "宿舍总分") private Double totalScore; + /** + * 扣分项明细 + */ + @ExcelProperty(value = "扣分项") + private String disciplineTypeName; + /** * 卫生等级 */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckDeductionVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckDeductionVo.java new file mode 100644 index 00000000..ca8dab58 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckDeductionVo.java @@ -0,0 +1,39 @@ +package org.dromara.xg.domain.vo; + +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.dromara.xg.domain.XgDormHygieneCheckDeduction; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 宿舍卫生检查扣分明细视图对象 + */ +@Data +@AutoMapper(target = XgDormHygieneCheckDeduction.class) +public class XgDormHygieneCheckDeductionVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long id; + + private Long hygieneCheckId; + + private Long disciplineTypeId; + + private String disciplineTypeName; + + private Double score; + + private Integer sortOrder; + + private String remark; + + /** 前端展示用:类型最低扣分 */ + private Double minScore; + + /** 前端展示用:类型最高扣分 */ + private Double maxScore; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckVo.java index bd50c81c..f970726b 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckVo.java @@ -13,6 +13,8 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; import java.util.Date; +import java.util.List; +import org.dromara.xg.domain.vo.XgDormHygieneCheckDeductionVo; /** * 宿舍卫生检查视图对象 xg_dorm_hygiene_check @@ -183,4 +185,9 @@ public class XgDormHygieneCheckVo implements Serializable { @ExcelProperty(value = "违纪类型名称") private String disciplineTypeName; + /** + * 扣分明细列表 + */ + private List deductionItems; + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneStudentScoreVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneStudentScoreVo.java index 48ef6e7b..3f87d30c 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneStudentScoreVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneStudentScoreVo.java @@ -113,6 +113,11 @@ public class XgDormHygieneStudentScoreVo implements Serializable { @ExcelProperty(value = "床位编号") private String bedCode; + /** + * 性别(0男 1女,来自xg_student_msg,查询时关联填充) + */ + private String gender; + /** * 学生个人得分(0-100分) */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgClassTeacherDormViewMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgClassTeacherDormViewMapper.java new file mode 100644 index 00000000..5e54cbf5 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgClassTeacherDormViewMapper.java @@ -0,0 +1,27 @@ +package org.dromara.xg.mapper; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; +import org.dromara.xg.domain.bo.ClassTeacherDormQueryBo; +import org.dromara.xg.domain.vo.ClassTeacherClassInfoVo; +import org.dromara.xg.domain.vo.ClassTeacherDormRollCallVo; +import org.dromara.xg.domain.vo.ClassTeacherDormViolationVo; + +/** + * 班主任宿舍情况查询 Mapper + */ +public interface XgClassTeacherDormViewMapper { + + ClassTeacherClassInfoVo selectClassByTeacherId(@Param("teacherId") Long teacherId); + + IPage selectViolationPage( + Page page, + @Param("classId") Long classId, + @Param("bo") ClassTeacherDormQueryBo bo); + + IPage selectRollCallPage( + Page page, + @Param("classId") Long classId, + @Param("bo") ClassTeacherDormQueryBo bo); +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormHygieneCheckDeductionMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormHygieneCheckDeductionMapper.java new file mode 100644 index 00000000..71161e8d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormHygieneCheckDeductionMapper.java @@ -0,0 +1,16 @@ +package org.dromara.xg.mapper; + +import org.apache.ibatis.annotations.Param; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.dromara.xg.domain.XgDormHygieneCheckDeduction; +import org.dromara.xg.domain.vo.XgDormHygieneCheckDeductionVo; + +import java.util.List; + +/** + * 宿舍卫生检查扣分明细Mapper接口 + */ +public interface XgDormHygieneCheckDeductionMapper extends BaseMapperPlus { + + List selectVoListByHygieneCheckId(@Param("hygieneCheckId") Long hygieneCheckId); +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormHygieneStudentScoreMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormHygieneStudentScoreMapper.java index d8650481..aaa529bf 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormHygieneStudentScoreMapper.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormHygieneStudentScoreMapper.java @@ -4,6 +4,10 @@ import org.dromara.xg.domain.XgDormHygieneStudentScore; import org.dromara.xg.domain.vo.XgDormHygieneStudentScoreVo; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + /** * 宿舍卫生检查学生评分Mapper接口 * @@ -12,4 +16,12 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; */ public interface XgDormHygieneStudentScoreMapper extends BaseMapperPlus { + /** + * 根据卫生检查ID查询学生评分列表(关联学生档案补充性别) + * + * @param hygieneCheckId 卫生检查ID + * @return 学生评分列表 + */ + List selectVoListByHygieneCheckId(@Param("hygieneCheckId") Long hygieneCheckId); + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgClassTeacherDormViewService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgClassTeacherDormViewService.java new file mode 100644 index 00000000..d4702e07 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgClassTeacherDormViewService.java @@ -0,0 +1,20 @@ +package org.dromara.xg.service; + +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.xg.domain.bo.ClassTeacherDormQueryBo; +import org.dromara.xg.domain.vo.ClassTeacherClassInfoVo; +import org.dromara.xg.domain.vo.ClassTeacherDormRollCallVo; +import org.dromara.xg.domain.vo.ClassTeacherDormViolationVo; + +/** + * 班主任宿舍情况查看 Service + */ +public interface IXgClassTeacherDormViewService { + + ClassTeacherClassInfoVo getMyClassInfo(); + + TableDataInfo queryViolationPage(ClassTeacherDormQueryBo bo, PageQuery pageQuery); + + TableDataInfo queryRollCallPage(ClassTeacherDormQueryBo bo, PageQuery pageQuery); +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormHygieneCheckDeductionService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormHygieneCheckDeductionService.java new file mode 100644 index 00000000..ff821208 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormHygieneCheckDeductionService.java @@ -0,0 +1,16 @@ +package org.dromara.xg.service; + +import org.dromara.xg.domain.bo.XgDormHygieneCheckDeductionBo; +import org.dromara.xg.domain.vo.XgDormHygieneCheckDeductionVo; + +import java.util.List; + +/** + * 宿舍卫生检查扣分明细Service接口 + */ +public interface IXgDormHygieneCheckDeductionService { + + List listByHygieneCheckId(Long hygieneCheckId); + + void replaceByHygieneCheckId(Long hygieneCheckId, List items); +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassTeacherDormViewServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassTeacherDormViewServiceImpl.java new file mode 100644 index 00000000..405d15da --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassTeacherDormViewServiceImpl.java @@ -0,0 +1,75 @@ +package org.dromara.xg.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.xg.domain.bo.ClassTeacherDormQueryBo; +import org.dromara.xg.domain.vo.ClassTeacherClassInfoVo; +import org.dromara.xg.domain.vo.ClassTeacherDormRollCallVo; +import org.dromara.xg.domain.vo.ClassTeacherDormViolationVo; +import org.dromara.xg.mapper.XgClassTeacherDormViewMapper; +import org.dromara.xg.service.IXgClassTeacherDormViewService; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * 班主任宿舍情况查看 Service 实现 + */ +@Service +@RequiredArgsConstructor +public class XgClassTeacherDormViewServiceImpl implements IXgClassTeacherDormViewService { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final XgClassTeacherDormViewMapper classTeacherDormViewMapper; + + @Override + public ClassTeacherClassInfoVo getMyClassInfo() { + Long teacherId = LoginHelper.getUserId(); + ClassTeacherClassInfoVo info = classTeacherDormViewMapper.selectClassByTeacherId(teacherId); + if (info == null) { + throw new ServiceException("当前账号未绑定班主任班级,无法查看"); + } + return info; + } + + @Override + public TableDataInfo queryViolationPage(ClassTeacherDormQueryBo bo, PageQuery pageQuery) { + Long classId = resolveClassId(); + normalizeDateRange(bo); + Page page = pageQuery.disableDefaultOrder().build(); + IPage result = classTeacherDormViewMapper.selectViolationPage(page, classId, bo); + return TableDataInfo.build(result); + } + + @Override + public TableDataInfo queryRollCallPage(ClassTeacherDormQueryBo bo, PageQuery pageQuery) { + Long classId = resolveClassId(); + normalizeDateRange(bo); + Page page = pageQuery.disableDefaultOrder().build(); + IPage result = classTeacherDormViewMapper.selectRollCallPage(page, classId, bo); + return TableDataInfo.build(result); + } + + private Long resolveClassId() { + ClassTeacherClassInfoVo info = getMyClassInfo(); + return info.getClassId(); + } + + private void normalizeDateRange(ClassTeacherDormQueryBo bo) { + String today = LocalDate.now().format(DATE_FMT); + if (StringUtils.isBlank(bo.getEndDate())) { + bo.setEndDate(today); + } + if (StringUtils.isBlank(bo.getBeginDate())) { + bo.setBeginDate(today); + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java index 8132cdb4..88c0db56 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java @@ -16,6 +16,7 @@ import org.dromara.xg.domain.vo.XgDormBedDetailVo; import org.dromara.xg.domain.XgDormBed; import org.dromara.xg.domain.XgDormRoom; import org.dromara.xg.domain.XgDormBedStudent; +import org.dromara.xg.domain.XgSysDept; import org.dromara.xg.domain.vo.XgStudentLeaveVo; import org.dromara.xg.mapper.XgDormBedMapper; import org.dromara.xg.mapper.XgDormRoomMapper; diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckDeductionServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckDeductionServiceImpl.java new file mode 100644 index 00000000..2e63402e --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckDeductionServiceImpl.java @@ -0,0 +1,57 @@ +package org.dromara.xg.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.xg.domain.XgDormHygieneCheckDeduction; +import org.dromara.xg.domain.bo.XgDormHygieneCheckDeductionBo; +import org.dromara.xg.domain.vo.XgDormHygieneCheckDeductionVo; +import org.dromara.xg.mapper.XgDormHygieneCheckDeductionMapper; +import org.dromara.xg.service.IXgDormHygieneCheckDeductionService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; + +/** + * 宿舍卫生检查扣分明细Service业务层处理 + */ +@RequiredArgsConstructor +@Service +public class XgDormHygieneCheckDeductionServiceImpl implements IXgDormHygieneCheckDeductionService { + + private final XgDormHygieneCheckDeductionMapper baseMapper; + + @Override + public List listByHygieneCheckId(Long hygieneCheckId) { + if (hygieneCheckId == null) { + return Collections.emptyList(); + } + return baseMapper.selectVoListByHygieneCheckId(hygieneCheckId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void replaceByHygieneCheckId(Long hygieneCheckId, List items) { + if (hygieneCheckId == null) { + return; + } + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + deleteWrapper.eq(XgDormHygieneCheckDeduction::getHygieneCheckId, hygieneCheckId); + baseMapper.delete(deleteWrapper); + + if (items == null || items.isEmpty()) { + return; + } + int sortOrder = 1; + for (XgDormHygieneCheckDeductionBo item : items) { + XgDormHygieneCheckDeduction entity = MapstructUtils.convert(item, XgDormHygieneCheckDeduction.class); + entity.setId(null); + entity.setHygieneCheckId(hygieneCheckId); + entity.setSortOrder(item.getSortOrder() != null ? item.getSortOrder() : sortOrder++); + baseMapper.insert(entity); + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java index d06141ed..16bc59ee 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java @@ -14,10 +14,12 @@ import org.springframework.transaction.annotation.Transactional; import org.dromara.xg.domain.bo.XgDormHygieneCheckBo; import org.dromara.xg.domain.bo.XgDormHygieneCheckCompleteBo; import org.dromara.xg.domain.bo.XgDormHygieneStudentScoreBo; +import org.dromara.xg.domain.bo.XgDormHygieneCheckDeductionBo; import org.dromara.xg.domain.bo.XgHygieneCheckViolationBo; import org.dromara.xg.domain.bo.XgViolationDataBo; import org.dromara.xg.domain.vo.DormHygieneCheckExportVo; import org.dromara.xg.domain.vo.XgDormHygieneCheckVo; +import org.dromara.xg.domain.vo.XgDormHygieneCheckDeductionVo; import org.dromara.xg.domain.XgDormHygieneCheck; import org.dromara.xg.domain.XgDormHygieneStudentScore; import org.dromara.xg.mapper.XgDormHygieneCheckMapper; @@ -26,6 +28,7 @@ import org.dromara.xg.service.IXgDormHygieneCheckService; import org.dromara.xg.service.IXgStudentMsgService; import org.dromara.xg.service.IXgHygieneCheckViolationService; import org.dromara.xg.service.IXgDormHygieneStudentScoreService; +import org.dromara.xg.service.IXgDormHygieneCheckDeductionService; import org.dromara.xg.mapper.XgHygieneCheckViolationMapper; import org.dromara.system.service.ISysDictDataService; import org.springframework.context.ApplicationContext; @@ -41,6 +44,10 @@ import java.util.stream.Collectors; import org.dromara.xg.domain.vo.XgHygieneCheckViolationVo; import org.dromara.xg.domain.XgHygieneCheckViolation; import java.util.Collections; +import java.util.ArrayList; +import org.dromara.common.core.exception.ServiceException; +import java.math.BigDecimal; +import java.math.RoundingMode; /** * 宿舍卫生检查Service业务层处理 @@ -60,6 +67,7 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService private final IXgHygieneCheckViolationService violationService; private final IXgDormHygieneStudentScoreService studentScoreService; private final XgHygieneCheckViolationMapper violationMapper; + private final IXgDormHygieneCheckDeductionService deductionService; private final ApplicationContext applicationContext; /** @@ -70,7 +78,21 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService */ @Override public XgDormHygieneCheckVo queryById(Long id) { - return baseMapper.selectVoById(id); + XgDormHygieneCheckVo vo = baseMapper.selectVoById(id); + if (vo == null) { + return null; + } + List deductionItems = deductionService.listByHygieneCheckId(id); + if (deductionItems.isEmpty() && vo.getDisciplineTypeId() != null) { + XgDormHygieneCheckDeductionVo legacyItem = new XgDormHygieneCheckDeductionVo(); + legacyItem.setDisciplineTypeId(vo.getDisciplineTypeId()); + legacyItem.setDisciplineTypeName(vo.getDisciplineTypeName()); + legacyItem.setScore(vo.getTotalScore()); + legacyItem.setSortOrder(1); + deductionItems = List.of(legacyItem); + } + vo.setDeductionItems(deductionItems); + return vo; } /** @@ -644,8 +666,10 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService @Transactional(rollbackFor = Exception.class) public Long saveCompleteData(XgDormHygieneCheckCompleteBo bo) { try { + List deductionItems = resolveDeductionItems(bo); // 1. 转换并保存主表数据 XgDormHygieneCheck add = MapstructUtils.convert(bo, XgDormHygieneCheck.class); + applyDeductionSummary(add, deductionItems); validEntityBeforeSave(add); boolean flag = baseMapper.insert(add) > 0; log.info("保存宿舍卫生检查主表数据: {}", add); @@ -657,6 +681,9 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService Long hygieneCheckId = add.getId(); + // 1.1 保存扣分明细 + deductionService.replaceByHygieneCheckId(hygieneCheckId, deductionItems); + // 2. 保存学生评分数据 if (bo.getStudentScores() != null && !bo.getStudentScores().isEmpty()) { for (XgDormHygieneStudentScoreBo studentScoreBo : bo.getStudentScores()) { @@ -901,8 +928,13 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService setFieldValue(disciplineRecordBo, "disciplineDate", java.time.LocalDate.now()); } - // 设置违纪原因和内容 - setFieldValue(disciplineRecordBo, "disciplineReason", violationBo.getDisciplineReason()); + // 设置违纪原因和内容(原因为空时使用违纪类型名称兜底,满足下游非空校验) + String disciplineReason = StringUtils.isNotBlank(violationBo.getDisciplineReason()) + ? violationBo.getDisciplineReason().trim() + : StringUtils.isNotBlank(violationBo.getDisciplineTypeName()) + ? violationBo.getDisciplineTypeName() + : "宿舍卫生检查违纪"; + setFieldValue(disciplineRecordBo, "disciplineReason", disciplineReason); setFieldValue(disciplineRecordBo, "disciplineContent", violationBo.getDisciplineContent()); // 设置扣分分值 @@ -1005,8 +1037,10 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService @Transactional(rollbackFor = Exception.class) public Boolean updateCompleteData(XgDormHygieneCheckCompleteBo bo) { try { + List deductionItems = resolveDeductionItems(bo); // 1. 更新主表数据 XgDormHygieneCheck update = MapstructUtils.convert(bo, XgDormHygieneCheck.class); + applyDeductionSummary(update, deductionItems); validEntityBeforeSave(update); boolean flag = baseMapper.updateById(update) > 0; log.info("更新宿舍卫生检查主表数据: {}", update); @@ -1018,6 +1052,9 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService Long hygieneCheckId = update.getId(); + // 1.1 更新扣分明细 + deductionService.replaceByHygieneCheckId(hygieneCheckId, deductionItems); + // 2. 更新学生评分数据 - 先删除旧数据,再插入新数据 LambdaQueryWrapper scoreWrapper = Wrappers.lambdaQuery(); scoreWrapper.eq(XgDormHygieneStudentScore::getHygieneCheckId, hygieneCheckId); @@ -1341,4 +1378,63 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService } } + private List resolveDeductionItems(XgDormHygieneCheckCompleteBo bo) { + if (bo.getDeductionItems() != null && !bo.getDeductionItems().isEmpty()) { + List items = new ArrayList<>(); + Set typeIds = new HashSet<>(); + int sortOrder = 1; + for (XgDormHygieneCheckDeductionBo item : bo.getDeductionItems()) { + if (item == null || item.getDisciplineTypeId() == null) { + continue; + } + if (!typeIds.add(item.getDisciplineTypeId())) { + continue; + } + XgDormHygieneCheckDeductionBo normalized = new XgDormHygieneCheckDeductionBo(); + normalized.setId(item.getId()); + normalized.setHygieneCheckId(bo.getId()); + normalized.setDisciplineTypeId(item.getDisciplineTypeId()); + normalized.setDisciplineTypeName(item.getDisciplineTypeName()); + normalized.setScore(item.getScore() != null ? item.getScore() : 0D); + normalized.setSortOrder(item.getSortOrder() != null ? item.getSortOrder() : sortOrder++); + normalized.setRemark(item.getRemark()); + items.add(normalized); + } + return items; + } + if (bo.getDisciplineTypeId() != null) { + XgDormHygieneCheckDeductionBo legacyItem = new XgDormHygieneCheckDeductionBo(); + legacyItem.setDisciplineTypeId(bo.getDisciplineTypeId()); + legacyItem.setDisciplineTypeName(bo.getDisciplineTypeName()); + legacyItem.setScore(bo.getTotalScore() != null ? bo.getTotalScore() : 0D); + legacyItem.setSortOrder(1); + return List.of(legacyItem); + } + return Collections.emptyList(); + } + + private void applyDeductionSummary(XgDormHygieneCheck entity, List items) { + if (items == null || items.isEmpty()) { + entity.setTotalScore(0D); + entity.setDisciplineTypeId(null); + entity.setDisciplineTypeName(null); + return; + } + double total = items.stream() + .map(item -> item.getScore() != null ? item.getScore() : 0D) + .mapToDouble(Double::doubleValue) + .sum(); + BigDecimal totalDecimal = BigDecimal.valueOf(total).setScale(2, RoundingMode.HALF_UP); + if (totalDecimal.doubleValue() > 100D) { + throw new ServiceException("宿舍合计扣分不能超过100分"); + } + entity.setTotalScore(totalDecimal.doubleValue()); + entity.setDisciplineTypeId(items.get(0).getDisciplineTypeId()); + String typeNames = items.stream() + .map(XgDormHygieneCheckDeductionBo::getDisciplineTypeName) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining("、")); + entity.setDisciplineTypeName(typeNames); + } + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneStudentScoreServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneStudentScoreServiceImpl.java index dea6878a..74510c05 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneStudentScoreServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneStudentScoreServiceImpl.java @@ -164,11 +164,7 @@ public class XgDormHygieneStudentScoreServiceImpl implements IXgDormHygieneStude */ @Override public List queryByHygieneCheckId(Long hygieneCheckId) { - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.eq(XgDormHygieneStudentScore::getHygieneCheckId, hygieneCheckId); - lqw.eq(XgDormHygieneStudentScore::getDeleted, 0); // 只查询未删除的记录 - lqw.orderByAsc(XgDormHygieneStudentScore::getId); - return baseMapper.selectVoList(lqw); + return baseMapper.selectVoListByHygieneCheckId(hygieneCheckId); } /** diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java index a9a5cb29..591d1dd6 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRollCallServiceImpl.java @@ -23,6 +23,8 @@ import org.dromara.xg.mapper.XgDormRollCallDetailMapper; import org.dromara.xg.mapper.XgDormRoomMapper; import org.dromara.xg.mapper.XgDormBedMapper; import org.dromara.xg.mapper.XgDormBuildingMapper; +import org.dromara.xg.mapper.XgStudentMsgMapper; +import org.dromara.xg.domain.XgStudentMsg; import org.dromara.xg.domain.XgDormRoom; import org.dromara.xg.domain.XgDormBuilding; import org.dromara.xg.domain.XgDormBed; @@ -45,6 +47,7 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { private final XgDormRoomMapper roomMapper; private final XgDormBedMapper bedMapper; private final XgDormBuildingMapper buildingMapper; + private final XgStudentMsgMapper studentMsgMapper; @Override public XgDormRollCallVo queryById(Long id) { @@ -274,6 +277,7 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { private void saveDetails(Long rollCallId, List details) { if (details == null || details.isEmpty()) return; for (XgDormRollCallDetailBo detailBo : details) { + fillDetailClassInfo(detailBo); XgDormRollCallDetail detail = new XgDormRollCallDetail(); detail.setRollCallId(rollCallId); detail.setBedId(detailBo.getBedId()); @@ -296,4 +300,35 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService { detailMapper.insert(detail); } } + + private void fillDetailClassInfo(XgDormRollCallDetailBo detailBo) { + if (detailBo.getStudentId() == null) { + return; + } + if (detailBo.getClassId() != null && StringUtils.isNotBlank(detailBo.getClassName())) { + return; + } + XgStudentMsg student = studentMsgMapper.selectById(detailBo.getStudentId()); + if (student == null) { + return; + } + if (detailBo.getClassId() == null && StringUtils.isNotBlank(student.getClassId())) { + detailBo.setClassId(Long.valueOf(student.getClassId())); + } + if (StringUtils.isBlank(detailBo.getClassName())) { + detailBo.setClassName(student.getClassName()); + } + if (detailBo.getMajorId() == null && StringUtils.isNotBlank(student.getMajorId())) { + detailBo.setMajorId(Long.valueOf(student.getMajorId())); + } + if (StringUtils.isBlank(detailBo.getMajorName())) { + detailBo.setMajorName(student.getMajorName()); + } + if (detailBo.getCollegeId() == null && StringUtils.isNotBlank(student.getCollegeId())) { + detailBo.setCollegeId(Long.valueOf(student.getCollegeId())); + } + if (StringUtils.isBlank(detailBo.getCollegeName())) { + detailBo.setCollegeName(student.getCollegeName()); + } + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/classTeacherDorm/XgClassTeacherDormViewMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/classTeacherDorm/XgClassTeacherDormViewMapper.xml new file mode 100644 index 00000000..7b96ebcc --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/classTeacherDorm/XgClassTeacherDormViewMapper.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckDeductionMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckDeductionMapper.xml new file mode 100644 index 00000000..252b8aa5 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckDeductionMapper.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckMapper.xml index 2b14d67a..71977c83 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgDormHygieneCheckMapper.xml @@ -15,6 +15,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -44,6 +45,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" dhc.building_name, dhc.room_name, dhc.total_score, + dhc.discipline_type_name, dhc.hygiene_grade, dhc.check_summary, dhc.improvement_suggestions, diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneStudentScore/XgDormHygieneStudentScoreMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneStudentScore/XgDormHygieneStudentScoreMapper.xml index a4df44e7..bb509a15 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneStudentScore/XgDormHygieneStudentScoreMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneStudentScore/XgDormHygieneStudentScoreMapper.xml @@ -4,4 +4,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + diff --git a/docs/开发文档/API/API文档总览.md b/docs/开发文档/API/API文档总览.md index 226d8b85..8e500790 100644 --- a/docs/开发文档/API/API文档总览.md +++ b/docs/开发文档/API/API文档总览.md @@ -178,6 +178,7 @@ | [班级干部信息管理API](./班级干部信息管理API.md) | 班级干部信息管理(含用户角色关联) | ✅ 完成 | 2025-01-27 | | [班干部管理API](./班干部管理API.md) | 班级干部职务管理 | ✅ 完成 | 2025-01-27 | | [班级教师关联管理API](./班级教师关联管理API.md) | 班主任辅导员关联关系 | ✅ 完成 | 2025-01-27 | +| [班主任班级宿舍情况查看API](./班主任班级宿舍情况查看API.md) | 班主任只读查看本班宿舍违纪与点名 | ✅ 完成 | 2026-06-23 | | **[学生考核管理系统API](./学生考核管理系统API.md)** | **学生综合素质测评、违纪处分、奖励加分管理** | **✅ 完成** | **2025-01-27** | | **[学生考核管理系统修改方案](./学生考核管理系统API修改方案.md)** | **基于标准化违纪处分等级的系统升级方案** | **📋 方案** | **2025-01-27** | | **[学生考核管理系统数据库修改影响分析](./学生考核管理系统数据库修改影响分析.md)** | **数据库升级脚本执行的详细影响分析** | **📊 分析** | **2025-01-27** | diff --git a/docs/开发文档/API/宿舍卫生检查管理API.md b/docs/开发文档/API/宿舍卫生检查管理API.md index b91b1190..db90defe 100644 --- a/docs/开发文档/API/宿舍卫生检查管理API.md +++ b/docs/开发文档/API/宿舍卫生检查管理API.md @@ -14,6 +14,27 @@ - 存储每个学生的个人评分信息 - 关联主表,包含学生信息、个人得分、出勤状态、评级等 +### 扣分明细表:xg_dorm_hygiene_check_deduction(2026-06-13 新增) +- 存储宿舍整体扣分的多条明细 +- 关联主表 `hygiene_check_id`,每条记录对应一个违纪类型及扣分值 +- 主表 `total_score` 为明细合计;`discipline_type_name` 为多项名称用「、」拼接(兼容旧列表/导出) + +**deductionItems 字段说明:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| disciplineTypeId | Long | 违纪类型ID | +| disciplineTypeName | String | 违纪类型名称 | +| score | Number | 该项扣分,默认取类型 minScore,可在 min-max 范围内手动调整 | +| sortOrder | Integer | 排序序号 | + +**业务规则:** +- 扣分项可选(允许无扣分项,totalScore=0) +- 每项默认分数为违纪类型的 `minScore` +- 单项分数必须在对应类型的 min-max 范围内 +- 合计扣分上限 100 分 +- 历史单条数据迁移:以主表 `total_score` 作为唯一明细的 score + ## API接口规范 ### 1. 新增宿舍卫生检查(包含学生评分) @@ -39,6 +60,20 @@ "campusId": 1, "campusName": "主校区", "totalScore": 85.5, + "deductionItems": [ + { + "disciplineTypeId": 1963793291531026434, + "disciplineTypeName": "地面未打扫", + "score": 5, + "sortOrder": 1 + }, + { + "disciplineTypeId": 1963793291531026435, + "disciplineTypeName": "物品摆放凌乱", + "score": 3, + "sortOrder": 2 + } + ], "hygieneGrade": "B", "attachmentIds": "oss1,oss2,oss3", "checkSummary": "宿舍整体卫生良好", @@ -280,7 +315,8 @@ public interface DormHygieneStudentScoreMapper extends BaseMapperPlus - - + + - - - - - - 【{{ formData.disciplineTypeName }}】类型说明:{{ - selectedDisciplineTypeRemark - }} - + + + + {{ item.disciplineTypeName }} + + {{ item.score }}分 + ({{ getDeductionMin(item) }}-{{ getDeductionMax(item) }}分) + + + + {{ getDeductionRemark(item.disciplineTypeId) }} + + + + 合计扣分:{{ formData.totalScore }} 分 + + + + + 选择扣分项 + 确定 + + + + + + + + + @@ -272,25 +304,9 @@ {{ student.studentName }} @@ -583,6 +599,8 @@ import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js'; import dayjs from 'dayjs'; import weChatSDK from '@/utils/wechat.js'; import ViolationRecordDialog from './ViolationRecordDialog.vue'; +import { normalizeStudentId, normalizeId } from '../utils/studentId.js'; +import { calcStudentViolationScoreTotal } from '../utils/violationScore.js'; export default { name: 'SingleRoomCheck', @@ -622,13 +640,16 @@ export default { attachmentIds: '', }, + deductionItems: [], + showDeductionPopup: false, + tempSelectedTypeIds: [], + // 表单验证规则 rules: { hygieneGrade: [{ required: true, message: '请选择卫生等级', trigger: 'change' }], }, // Picker显示状态 - showDisciplineTypePicker: false, showHygieneGradePicker: false, // 违纪类型选项 @@ -692,32 +713,9 @@ export default { return name; }, - // 违纪类型列 - disciplineTypeColumns() { - return [ - this.disciplineTypeOptions.map((item) => ({ - text: item.typeName, - value: item.id, - })), - ]; - }, - - // 违纪类型显示文本 - disciplineTypeDisplay() { - if (!this.formData.disciplineTypeId) return ''; - const item = this.disciplineTypeOptions.find( - (d) => d.id === this.formData.disciplineTypeId, - ); - return item ? item.typeName : ''; - }, - - // 违纪类型说明 - selectedDisciplineTypeRemark() { - if (!this.formData.disciplineTypeId) return ''; - const item = this.disciplineTypeOptions.find( - (d) => d.id === this.formData.disciplineTypeId, - ); - return item ? item.remark : ''; + deductionSummaryDisplay() { + if (!this.deductionItems.length) return ''; + return this.deductionItems.map((item) => item.disciplineTypeName).join('、'); }, // 卫生等级列 @@ -822,12 +820,10 @@ export default { // 加载违纪类型 await this.loadDisciplineTypes(); - // 加载房间学生信息 - await this.loadRoomStudents(); - - // 如果有现有数据,加载编辑数据 - if (this.existingData) { - this.loadEditData(); + if (this.existingData?.id) { + await this.loadEditData(); + } else { + await this.loadRoomStudents(); } } catch (error) { console.error('初始化组件失败:', error); @@ -850,6 +846,52 @@ export default { } }, + normalizeGender(gender) { + if (gender === null || gender === undefined || gender === '') { + return undefined; + } + const value = String(gender); + if (value === '0' || value === '男') { + return '0'; + } + if (value === '1' || value === '女') { + return '1'; + } + return value; + }, + + mergeGenderFromRoomStudents(students, roomStudents) { + if (!students.length || !roomStudents.length) { + return students; + } + + const genderByStudentId = {}; + const genderByStudentNo = {}; + roomStudents.forEach((student) => { + const gender = this.normalizeGender(student.gender); + if (!gender) { + return; + } + if (student.studentId !== undefined && student.studentId !== null) { + genderByStudentId[String(student.studentId)] = gender; + } + if (student.studentNo) { + genderByStudentNo[student.studentNo] = gender; + } + }); + + return students.map((student) => { + if (this.normalizeGender(student.gender)) { + return { ...student, gender: this.normalizeGender(student.gender) }; + } + const gender = + (student.studentId !== undefined && student.studentId !== null + ? genderByStudentId[String(student.studentId)] + : undefined) || (student.studentNo ? genderByStudentNo[student.studentNo] : undefined); + return gender ? { ...student, gender } : student; + }); + }, + // 加载违纪类型 async loadDisciplineTypes() { try { @@ -897,6 +939,7 @@ export default { this.roomStudents = res.data.map((student) => ({ ...student, + studentId: normalizeStudentId(student.studentId), student_score: 0, attendance_status: '1', student_grade: 'A', @@ -937,10 +980,10 @@ export default { if (res.code === 200 && res.data) { this.studentLeaves.clear(); Object.keys(res.data).forEach((studentId) => { - this.studentLeaves.set(Number(studentId), res.data[studentId]); - // 自动设置出勤状态为请假 + const normalizedId = normalizeStudentId(studentId); + this.studentLeaves.set(normalizedId, res.data[studentId]); const student = this.roomStudents.find( - (s) => s.studentId === Number(studentId), + (s) => normalizeStudentId(s.studentId) === normalizedId, ); if (student) { student.attendance_status = '3'; @@ -969,6 +1012,7 @@ export default { this.formData.hygieneGrade = this.existingData.hygieneGrade || 'A'; this.formData.checkRemarks = this.existingData.checkRemarks || ''; this.formData.attachmentIds = this.existingData.attachmentIds || ''; + this.initDeductionItemsFromData(this.existingData); console.log('✅ 表单基础数据回显完成:', this.formData); @@ -980,7 +1024,7 @@ export default { // 3️⃣ 从后端接口加载学生评分数据、违纪记录和请假信息(复用Web端接口) // 注意:loadStudentScoresFromAPI 内部会自动调用 loadViolationRecordsFromAPI 和 loadStudentLeaves if (this.existingData.id) { - await this.loadStudentScoresFromAPI(this.existingData.id); + await this.loadStudentScoresFromAPI(normalizeId(this.existingData.id)); } console.log('✅ 所有数据回显完成'); @@ -992,17 +1036,18 @@ export default { // 从后端API加载学生评分数据(复用Web端接口) async loadStudentScoresFromAPI(checkId) { - if (!checkId) { + const normalizedCheckId = normalizeId(checkId); + if (!normalizedCheckId) { console.warn('⚠️ 没有检查ID,无法加载学生评分数据'); return; } this.studentsLoading = true; try { - console.log('🔄 从API加载学生评分数据,检查ID:', checkId); + console.log('🔄 从API加载学生评分数据,检查ID:', normalizedCheckId); const res = await request({ - url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${checkId}`, + url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${normalizedCheckId}`, method: 'GET', }); @@ -1010,9 +1055,9 @@ export default { console.log('✅ 成功获取学生评分数据:', res.data.length, '条'); // 将后端返回的学生评分数据映射到roomStudents - this.roomStudents = res.data.map((studentScore) => ({ + let students = res.data.map((studentScore) => ({ // 学生基本信息 - studentId: studentScore.studentId, + studentId: normalizeStudentId(studentScore.studentId), studentName: studentScore.studentName, studentNo: studentScore.studentNo, className: studentScore.className, @@ -1021,7 +1066,7 @@ export default { bedId: studentScore.bedId, bedName: studentScore.bedCode, bedCode: studentScore.bedCode, - gender: studentScore.gender || '1', + gender: this.normalizeGender(studentScore.gender), // 评分数据 student_score: studentScore.studentScore || 0, attendance_status: studentScore.attendanceStatus || '1', @@ -1034,13 +1079,27 @@ export default { showGradePicker: false, })); + if (students.some((student) => !student.gender) && this.roomInfo && this.roomInfo.id) { + const roomRes = await request({ + url: '/dormBedStudent/dormBedStudent/getRoomStudents', + method: 'GET', + params: { roomId: this.roomInfo.id }, + }); + if (roomRes.code === 200 && roomRes.data) { + students = this.mergeGenderFromRoomStudents(students, roomRes.data); + } + } + + this.roomStudents = students; + console.log('✅ 学生评分数据映射完成'); // 🔴 同时加载违纪记录和请假信息(复用Web端逻辑) await Promise.all([ - this.loadViolationRecordsFromAPI(checkId), + this.loadViolationRecordsFromAPI(normalizedCheckId), this.loadStudentLeaves(), ]); + this.syncStudentScoreFromViolations(); } else { console.warn('⚠️ 未获取到学生评分数据,保持当前学生列表'); } @@ -1054,16 +1113,17 @@ export default { // 从后端API加载违纪记录数据(复用Web端接口) async loadViolationRecordsFromAPI(checkId) { - if (!checkId) { + const normalizedCheckId = normalizeId(checkId); + if (!normalizedCheckId) { console.warn('⚠️ 没有检查ID,无法加载违纪记录数据'); return; } try { - console.log('🔄 从API加载违纪记录数据,检查ID:', checkId); + console.log('🔄 从API加载违纪记录数据,检查ID:', normalizedCheckId); const res = await request({ - url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${checkId}`, + url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${normalizedCheckId}`, method: 'GET', }); @@ -1075,7 +1135,7 @@ export default { // 按学生ID分组违纪记录 res.data.forEach((violation) => { - const studentId = violation.studentId; + const studentId = normalizeStudentId(violation.studentId); if (!studentId) return; const violations = this.violationRecords.get(studentId) || []; @@ -1101,6 +1161,7 @@ export default { '✅ 所有违纪记录数据加载完成,总学生数:', this.violationRecords.size, ); + this.syncStudentScoreFromViolations(); } else { console.warn('⚠️ 未获取到违纪记录数据'); } @@ -1318,7 +1379,9 @@ export default { console.log('👥 回显学生评分数据:', studentScores); studentScores.forEach((scoreData) => { - const student = this.roomStudents.find((s) => s.studentId === scoreData.studentId); + const student = this.roomStudents.find( + (s) => normalizeStudentId(s.studentId) === normalizeStudentId(scoreData.studentId), + ); if (student) { student.student_score = scoreData.studentScore || 0; student.attendance_status = scoreData.attendanceStatus || '1'; @@ -1337,7 +1400,7 @@ export default { this.violationRecords.clear(); violationData.forEach((violation) => { - const studentId = violation.studentId; + const studentId = normalizeStudentId(violation.studentId); if (!studentId) return; const violations = this.violationRecords.get(studentId) || []; @@ -1360,6 +1423,28 @@ export default { violation.violationAttachmentIds || violation.attachmentOssIds, ); }); + this.syncStudentScoreFromViolations(); + }, + + // 根据违纪记录合计同步个人扣分 + syncStudentScoreFromViolations(studentId) { + const syncOne = (sid) => { + const student = this.roomStudents.find( + (item) => normalizeStudentId(item.studentId) === sid, + ); + if (student) { + student.student_score = calcStudentViolationScoreTotal(this.getStudentViolations(sid)); + } + }; + + if (studentId != null && studentId !== '') { + syncOne(normalizeStudentId(studentId)); + return; + } + + this.roomStudents.forEach((student) => { + syncOne(normalizeStudentId(student.studentId)); + }); }, // 清理blob URL,防止内存泄漏 @@ -1377,38 +1462,104 @@ export default { }); }, - // 违纪类型确认处理 - handleDisciplineTypeConfirm(e) { - if (e && e.value && e.value.length > 0) { - const selectedType = this.disciplineTypeOptions.find( - (item) => item.id === e.value[0].value, - ); - if (selectedType) { - this.formData.disciplineTypeId = selectedType.id; - this.formData.disciplineTypeName = selectedType.typeName; - - // 自动填入最低扣分 - if (selectedType.minScore !== null && selectedType.minScore !== undefined) { - this.formData.totalScore = selectedType.minScore; - uni.showToast({ - title: `已自动填入最低扣分:${selectedType.minScore}分`, - icon: 'none', - }); - } - } + initDeductionItemsFromData(data) { + if (data?.deductionItems && data.deductionItems.length > 0) { + this.deductionItems = data.deductionItems.map((item, index) => ({ + ...item, + score: item.score ?? 0, + sortOrder: item.sortOrder ?? index + 1, + })); + } else if (data?.disciplineTypeId) { + this.deductionItems = [ + { + disciplineTypeId: data.disciplineTypeId, + disciplineTypeName: data.disciplineTypeName, + score: data.totalScore || 0, + sortOrder: 1, + }, + ]; + } else { + this.deductionItems = []; } - this.showDisciplineTypePicker = false; + this.recalcDeductionTotal(); }, - // 宿舍总分变化处理 - handleTotalScoreChange() { - const score = parseFloat(this.formData.totalScore); - if (!isNaN(score) && (score < 0 || score > 100)) { - uni.showToast({ title: '宿舍扣分必须在0-100分之间', icon: 'none' }); - this.formData.totalScore = 0; + openDeductionPopup() { + this.tempSelectedTypeIds = this.deductionItems.map((item) => String(item.disciplineTypeId)); + this.showDeductionPopup = true; + }, + + confirmDeductionSelection() { + const currentMap = new Map( + this.deductionItems.map((item) => [String(item.disciplineTypeId), item]), + ); + const nextItems = []; + this.tempSelectedTypeIds.forEach((typeId, index) => { + const option = this.disciplineTypeOptions.find((item) => String(item.id) === String(typeId)); + if (!option) return; + const existing = currentMap.get(String(typeId)); + nextItems.push({ + disciplineTypeId: option.id, + disciplineTypeName: option.typeName, + score: existing ? existing.score : Number(option.minScore || 0), + sortOrder: index + 1, + }); + }); + this.deductionItems = nextItems; + this.recalcDeductionTotal(); + this.showDeductionPopup = false; + }, + + removeDeductionItem(index) { + this.deductionItems.splice(index, 1); + this.recalcDeductionTotal(); + }, + + handleDeductionScoreChange() { + this.deductionItems.forEach((item) => { + let score = parseFloat(item.score); + if (isNaN(score)) { + score = this.getDeductionMin(item); + } + const min = this.getDeductionMin(item); + const max = this.getDeductionMax(item); + if (score < min) score = min; + if (score > max) score = max; + item.score = score; + }); + this.recalcDeductionTotal(); + if ((this.formData.totalScore || 0) > 100) { + uni.showToast({ title: '合计扣分不能超过100分', icon: 'none' }); } }, + recalcDeductionTotal() { + const total = this.deductionItems.reduce( + (sum, item) => sum + (parseFloat(item.score) || 0), + 0, + ); + this.formData.totalScore = Number(total.toFixed(2)); + }, + + getDeductionOption(typeId) { + return this.disciplineTypeOptions.find((item) => String(item.id) === String(typeId)); + }, + + getDeductionMin(item) { + const option = this.getDeductionOption(item.disciplineTypeId); + return option?.minScore ?? 0; + }, + + getDeductionMax(item) { + const option = this.getDeductionOption(item.disciplineTypeId); + return option?.maxScore ?? 100; + }, + + getDeductionRemark(typeId) { + const option = this.getDeductionOption(typeId); + return option?.remark || ''; + }, + // 卫生等级确认处理 handleHygieneGradeConfirm(e) { if (e && e.value && e.value.length > 0) { @@ -2114,7 +2265,7 @@ export default { // 获取学生请假信息 getStudentLeave(studentId) { - return this.studentLeaves.get(studentId) || null; + return this.studentLeaves.get(normalizeStudentId(studentId)) || null; }, // 格式化日期时间 @@ -2141,8 +2292,7 @@ export default { // 删除违纪记录 removeViolationRecord(studentId, index) { - // 保存要删除的记录信息 - this.deleteViolationStudentId = studentId; + this.deleteViolationStudentId = normalizeStudentId(studentId); this.deleteViolationIndex = index; // 显示确认对话框 this.showDeleteConfirm = true; @@ -2159,6 +2309,7 @@ export default { } else { this.violationRecords.set(this.deleteViolationStudentId, violations); } + this.syncStudentScoreFromViolations(this.deleteViolationStudentId); uni.showToast({ title: '违纪记录删除成功', icon: 'success' }); } @@ -2178,25 +2329,31 @@ export default { // 获取学生违纪记录 getStudentViolations(studentId) { - return this.violationRecords.get(studentId) || []; + return this.violationRecords.get(normalizeStudentId(studentId)) || []; }, // 违纪记录确认 handleViolationConfirm(violationData) { - const studentId = this.currentStudent.studentId; + const studentId = normalizeStudentId(this.currentStudent.studentId); const violations = this.violationRecords.get(studentId) || []; - if (this.currentViolationIndex >= 0) { - // 编辑模式 + if (Array.isArray(violationData)) { + violations.push(...violationData); + uni.showToast({ + title: `已添加${violationData.length}条违纪记录`, + icon: 'success', + }); + } else if (this.currentViolationIndex >= 0) { violations[this.currentViolationIndex] = violationData; + uni.showToast({ title: '违纪记录保存成功', icon: 'success' }); } else { - // 新增模式 violations.push(violationData); + uni.showToast({ title: '违纪记录保存成功', icon: 'success' }); } this.violationRecords.set(studentId, violations); + this.syncStudentScoreFromViolations(studentId); this.violationDialogVisible = false; - uni.showToast({ title: '违纪记录保存成功', icon: 'success' }); }, // 违纪记录弹窗关闭 @@ -2210,8 +2367,15 @@ export default { // 表单验证 await this.$refs.formRef.validate(); + if ((this.formData.totalScore || 0) > 100) { + uni.showToast({ title: '宿舍合计扣分不能超过100分', icon: 'none' }); + return; + } + this.submitLoading = true; + this.syncStudentScoreFromViolations(); + // 处理附件数据 const attachmentOssIds = this.attachmentList .filter((file) => file.status === 'success' && file.ossId) @@ -2270,6 +2434,7 @@ export default { // 宿舍整体评分 ...this.formData, + deductionItems: this.deductionItems, // 学生评分数据 studentScores, @@ -2791,4 +2956,90 @@ export default { padding: 20rpx 30rpx; padding-bottom: 40rpx; } + +.deduction-list { + margin: 16rpx 0 24rpx; + padding: 20rpx; + background: #f8f9fb; + border-radius: 12rpx; +} + +.deduction-item + .deduction-item { + margin-top: 16rpx; + padding-top: 16rpx; + border-top: 1rpx dashed #e4e7ed; +} + +.deduction-item-header { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12rpx; +} + +.deduction-name { + font-size: 26rpx; + font-weight: 500; + color: #303133; + min-width: 160rpx; +} + +.deduction-score { + font-size: 26rpx; + color: #f56c6c; + font-weight: 600; +} + +.deduction-range { + font-size: 22rpx; + color: #909399; +} + +.deduction-remark { + margin-top: 8rpx; + font-size: 22rpx; + color: #909399; + line-height: 1.5; +} + +.deduction-total { + margin-top: 16rpx; + padding-top: 16rpx; + border-top: 1rpx solid #e4e7ed; + font-size: 26rpx; + color: #606266; +} + +.deduction-total .total-value { + color: #f56c6c; + font-weight: 600; + font-size: 30rpx; +} + +.deduction-popup { + padding: 24rpx 30rpx 40rpx; + max-height: 70vh; +} + +.deduction-popup-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; +} + +.deduction-popup-title { + font-size: 32rpx; + font-weight: 600; + color: #303133; +} + +.deduction-popup-action { + font-size: 28rpx; + color: #3c9cff; +} + +.deduction-popup-body { + max-height: 55vh; +} diff --git a/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue b/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue index 502a5e29..8d82ffc7 100644 --- a/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue +++ b/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue @@ -36,40 +36,120 @@ - - - - + + - - + + @@ -93,8 +173,13 @@ > - - + + - + item.disciplineTypeName).join('、'); + }, + + violationItemsTotalScore() { + const sum = this.violationItems.reduce( + (acc, item) => acc + (parseFloat(item.scoreDeduction) || 0), + 0, + ); + return Number(sum.toFixed(2)); + }, + // 违纪类型列(带区域标签) disciplineTypeColumns() { return [ @@ -768,8 +873,9 @@ export default { } } else { console.log('违纪记录:新增模式,重置附件列表'); - // 重置附件列表 this.attachmentList = []; + this.violationItems = []; + this.tempSelectedTypeIds = []; } }, @@ -919,6 +1025,11 @@ export default { // 违纪类型点击处理 handleDisciplineTypeClick() { + if (!this.isEditMode) { + this.openViolationTypePopup(); + return; + } + console.log('=== 违纪类型点击 ==='); console.log('违纪类型数据量:', this.allViolationTypes.length); console.log('违纪类型数据:', this.allViolationTypes); @@ -996,6 +1107,100 @@ export default { this.showDisciplineTypePicker = false; }, + openViolationTypePopup() { + if (this.allViolationTypes.length === 0) { + uni.showToast({ + title: '暂无可用的违纪类型', + icon: 'none', + }); + return; + } + this.tempSelectedTypeIds = this.violationItems.map((item) => String(item.disciplineTypeId)); + this.showViolationTypePopup = true; + }, + + confirmViolationTypeSelection() { + const currentMap = new Map( + this.violationItems.map((item) => [String(item.disciplineTypeId), item]), + ); + const nextItems = []; + this.tempSelectedTypeIds.forEach((typeId) => { + const option = this.allViolationTypes.find((item) => String(item.id) === String(typeId)); + if (!option) return; + const existing = currentMap.get(String(typeId)); + nextItems.push({ + disciplineTypeId: option.id, + disciplineTypeName: option.typeName, + scoreDeduction: existing + ? existing.scoreDeduction + : parseFloat(option.minScore) || 0, + }); + }); + this.violationItems = nextItems; + this.showViolationTypePopup = false; + }, + + removeViolationItem(index) { + this.violationItems.splice(index, 1); + }, + + getViolationOption(typeId) { + return this.allViolationTypes.find((item) => String(item.id) === String(typeId)); + }, + + getViolationMin(item) { + const option = this.getViolationOption(item.disciplineTypeId); + return option?.minScore ?? 0; + }, + + getViolationMax(item) { + const option = this.getViolationOption(item.disciplineTypeId); + return option?.maxScore ?? 100; + }, + + getViolationRemark(typeId) { + const option = this.getViolationOption(typeId); + return option?.remark || ''; + }, + + handleViolationScoreChange() { + this.violationItems.forEach((item) => { + let score = parseFloat(item.scoreDeduction); + if (isNaN(score)) { + score = this.getViolationMin(item); + } + const min = this.getViolationMin(item); + const max = this.getViolationMax(item); + if (score < min) score = min; + if (score > max) score = max; + item.scoreDeduction = score; + }); + }, + + buildSharedViolationFields() { + const attachmentOssIds = this.attachmentList + .filter((file) => file.status === 'success' && file.ossId) + .map((file) => file.ossId) + .join(','); + + return { + studentId: this.formData.studentId, + studentNo: this.formData.studentNo, + studentName: this.formData.studentName, + collegeId: this.formData.collegeId, + collegeName: this.formData.collegeName, + majorId: this.formData.majorId, + majorName: this.formData.majorName, + classId: this.formData.classId, + className: this.formData.className, + disciplineDate: this.formData.disciplineDate, + disciplineReason: this.formData.disciplineReason, + disciplineContent: this.formData.disciplineContent, + attachmentOssIds, + violationSource: this.formData.violationSource || '1', + }; + }, + // 扣分变化处理 handleScoreChange() { // 确保转换为数字类型 @@ -1844,18 +2049,55 @@ export default { // 确定 async handleConfirm() { try { - // 表单验证 - await this.$refs.formRef.validate(); - this.confirmLoading = true; - // 处理附件数据 - const attachmentOssIds = this.attachmentList - .filter((file) => file.status === 'success' && file.ossId) - .map((file) => file.ossId); - this.formData.attachmentOssIds = attachmentOssIds.join(','); + if (this.isEditMode) { + await this.$refs.formRef.validate(); + const attachmentOssIds = this.attachmentList + .filter((file) => file.status === 'success' && file.ossId) + .map((file) => file.ossId); + this.formData.attachmentOssIds = attachmentOssIds.join(','); + this.$emit('confirm', { ...this.formData }); + return; + } - this.$emit('confirm', { ...this.formData }); + if (!this.formData.disciplineDate) { + uni.showToast({ title: '请选择违纪日期', icon: 'none' }); + return; + } + if (this.violationItems.length === 0) { + uni.showToast({ title: '请至少选择一个违纪类型', icon: 'none' }); + return; + } + + for (const item of this.violationItems) { + const min = this.getViolationMin(item); + const max = this.getViolationMax(item); + const score = parseFloat(item.scoreDeduction); + if (isNaN(score)) { + uni.showToast({ + title: `请填写「${item.disciplineTypeName}」扣分`, + icon: 'none', + }); + return; + } + if (score < min || score > max) { + uni.showToast({ + title: `「${item.disciplineTypeName}」须在${min}-${max}分`, + icon: 'none', + }); + return; + } + } + + const sharedFields = this.buildSharedViolationFields(); + const records = this.violationItems.map((item) => ({ + ...sharedFields, + disciplineTypeId: item.disciplineTypeId, + disciplineTypeName: item.disciplineTypeName, + scoreDeduction: item.scoreDeduction, + })); + this.$emit('confirm', records); } catch (error) { console.error('提交失败:', error); uni.showToast({ title: '请检查表单填写是否完整', icon: 'none' }); @@ -1915,6 +2157,9 @@ export default { this.currentTypeMaxScore = 100; this.disciplineDateDisplay = dayjs().format('YYYY-MM-DD'); this.disciplineDateValue = Number(new Date()); + this.violationItems = []; + this.tempSelectedTypeIds = []; + this.showViolationTypePopup = false; }, }, }; @@ -2200,4 +2445,76 @@ export default { padding-bottom: 40rpx; border-top: 2rpx solid #f0f0f0; } + +.violation-item-list { + margin: 0 0 24rpx; + padding: 20rpx; + background: #f8f9fb; + border-radius: 12rpx; +} + +.violation-item-row + .violation-item-row { + margin-top: 16rpx; + padding-top: 16rpx; + border-top: 1rpx dashed #e4e7ed; +} + +.violation-item-header { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12rpx; +} + +.violation-item-name { + font-size: 26rpx; + font-weight: 500; + color: #303133; + min-width: 160rpx; +} + +.score-range { + font-size: 22rpx; + color: #909399; +} + +.violation-item-total { + margin-top: 16rpx; + padding-top: 16rpx; + border-top: 1rpx solid #e4e7ed; + font-size: 26rpx; + color: #606266; +} + +.violation-item-total .total-value { + color: #f56c6c; + font-weight: 600; +} + +.violation-type-popup { + padding: 24rpx 30rpx 40rpx; + max-height: 70vh; +} + +.violation-type-popup-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; +} + +.violation-type-popup-title { + font-size: 32rpx; + font-weight: 600; + color: #303133; +} + +.violation-type-popup-action { + font-size: 28rpx; + color: #3c9cff; +} + +.violation-type-popup-body { + max-height: 55vh; +} diff --git a/mobile/src/pages/xgss/wsjc/utils/studentId.js b/mobile/src/pages/xgss/wsjc/utils/studentId.js new file mode 100644 index 00000000..81c256ec --- /dev/null +++ b/mobile/src/pages/xgss/wsjc/utils/studentId.js @@ -0,0 +1,10 @@ +/** 雪花ID统一转字符串,避免 JS Number 精度丢失导致 Map/接口匹配失败 */ +export function normalizeStudentId(studentId) { + if (studentId === null || studentId === undefined || studentId === '') { + return ''; + } + return String(studentId); +} + +/** 通用雪花ID归一化(检查ID、学生ID等) */ +export const normalizeId = normalizeStudentId; diff --git a/mobile/src/pages/xgss/wsjc/utils/violationScore.js b/mobile/src/pages/xgss/wsjc/utils/violationScore.js new file mode 100644 index 00000000..a0f0e953 --- /dev/null +++ b/mobile/src/pages/xgss/wsjc/utils/violationScore.js @@ -0,0 +1,7 @@ +/** 计算学生有效违纪记录的扣分合计(已撤销 status=2 不计入) */ +export function calcStudentViolationScoreTotal(violations) { + const total = (violations || []) + .filter((item) => item && item.status !== '2') + .reduce((sum, item) => sum + (parseFloat(String(item.scoreDeduction ?? 0)) || 0), 0); + return parseFloat(total.toFixed(2)); +} diff --git a/mobile/src/pages/xgss/wsjc/wsjc.vue b/mobile/src/pages/xgss/wsjc/wsjc.vue index d1854000..454fe82f 100644 --- a/mobile/src/pages/xgss/wsjc/wsjc.vue +++ b/mobile/src/pages/xgss/wsjc/wsjc.vue @@ -270,6 +270,7 @@ import { request } from '@/common/request.js'; import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js'; import SingleRoomCheck from './components/SingleRoomCheck.vue'; +import { normalizeId } from './utils/studentId.js'; export default { name: 'DormHygieneCheck', @@ -544,7 +545,7 @@ export default { // 加载检查记录详情 const res = await request({ - url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`, + url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`, method: 'GET', }); @@ -552,7 +553,10 @@ export default { if (res && res.code === 200 && res.data) { this.currentViewItem = item; - this.currentViewData = res.data; + this.currentViewData = { + ...res.data, + id: normalizeId(res.data.id), + }; // 构造房间信息 this.currentViewRoomInfo = { @@ -605,7 +609,7 @@ export default { // 加载检查记录详情 const res = await request({ - url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`, + url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`, method: 'GET', }); @@ -613,7 +617,10 @@ export default { if (res && res.code === 200 && res.data) { this.currentEditItem = item; - this.currentEditData = res.data; + this.currentEditData = { + ...res.data, + id: normalizeId(res.data.id), + }; // 构造房间信息 this.currentRoomInfo = { @@ -708,7 +715,7 @@ export default { uni.showLoading({ title: '删除中...' }); const res = await request({ - url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`, + url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`, method: 'DELETE', }); diff --git a/mobile/src/pages/xgss/wsjc/wsjcDetail.vue b/mobile/src/pages/xgss/wsjc/wsjcDetail.vue index ad0ebc8d..ffc34492 100644 --- a/mobile/src/pages/xgss/wsjc/wsjcDetail.vue +++ b/mobile/src/pages/xgss/wsjc/wsjcDetail.vue @@ -1309,6 +1309,52 @@ export default { } }, + normalizeGender(gender) { + if (gender === null || gender === undefined || gender === '') { + return undefined; + } + const value = String(gender); + if (value === '0' || value === '男') { + return '0'; + } + if (value === '1' || value === '女') { + return '1'; + } + return value; + }, + + mergeGenderFromRoomStudents(students, roomStudents) { + if (!students.length || !roomStudents.length) { + return students; + } + + const genderByStudentId = {}; + const genderByStudentNo = {}; + roomStudents.forEach((student) => { + const gender = this.normalizeGender(student.gender); + if (!gender) { + return; + } + if (student.studentId !== undefined && student.studentId !== null) { + genderByStudentId[String(student.studentId)] = gender; + } + if (student.studentNo) { + genderByStudentNo[student.studentNo] = gender; + } + }); + + return students.map((student) => { + if (this.normalizeGender(student.gender)) { + return { ...student, gender: this.normalizeGender(student.gender) }; + } + const gender = + (student.studentId !== undefined && student.studentId !== null + ? genderByStudentId[String(student.studentId)] + : undefined) || (student.studentNo ? genderByStudentNo[student.studentNo] : undefined); + return gender ? { ...student, gender } : student; + }); + }, + // 加载房间学生信息 async loadRoomStudents() { if (!this.formData.roomId) { @@ -1329,8 +1375,9 @@ export default { studentScoresRes.data && studentScoresRes.data.length > 0 ) { - this.roomStudents = studentScoresRes.data.map((item) => ({ + let students = studentScoresRes.data.map((item) => ({ ...item, + gender: this.normalizeGender(item.gender), student_score: item.studentScore || null, attendance_status: item.attendanceStatus || '1', student_grade: item.studentGrade || 'A', @@ -1339,6 +1386,19 @@ export default { bedCode: item.bedCode, bedName: item.bedCode, })); + + if (students.some((student) => !student.gender) && this.formData.roomId) { + const roomRes = await request({ + url: '/dormBedStudent/dormBedStudent/getRoomStudents', + method: 'GET', + params: { roomId: this.formData.roomId }, + }); + if (roomRes.code === 200 && roomRes.data) { + students = this.mergeGenderFromRoomStudents(students, roomRes.data); + } + } + + this.roomStudents = students; return; } } diff --git a/web/src/api/xg/xgss/classTeacherDorm/index.ts b/web/src/api/xg/xgss/classTeacherDorm/index.ts new file mode 100644 index 00000000..3b0da2f1 --- /dev/null +++ b/web/src/api/xg/xgss/classTeacherDorm/index.ts @@ -0,0 +1,94 @@ +import request from '@/utils/request'; +import { AxiosPromise } from 'axios'; +import { TableDataInfo } from '@/api/types'; + +export interface ClassTeacherClassInfo { + classId: string | number; + className: string; + classCode?: string; + collegeName?: string; + majorName?: string; +} + +export interface ClassTeacherDormQuery { + beginDate?: string; + endDate?: string; + studentNo?: string; + studentName?: string; + rollCallStatus?: string; + pageNum?: number; + pageSize?: number; +} + +export interface ClassTeacherDormViolationVO { + id: string | number; + dataSource: string; + dataSourceName: string; + studentNo: string; + studentName: string; + className: string; + buildingName: string; + roomName: string; + violationTypeName: string; + scoreDeduction?: number; + violationLevel?: string; + violationDate: string; + violationReason?: string; + status?: string; + createTime?: string; +} + +export interface ClassTeacherDormRollCallVO { + id: string | number; + studentNo: string; + studentName: string; + className: string; + batchNo: string; + checkDate: string; + checkerName: string; + buildingName: string; + roomName: string; + bedCode: string; + rollCallStatus: string; + leaveTypeName?: string; + remark?: string; +} + +/** 校验业务 code(GET 请求失败时拦截器不会 reject,需手动检查) */ +function unwrapResponse(res: any, action: string): T { + const code = res?.code; + if (code !== undefined && code !== null && code !== 200 && code !== 0) { + throw new Error(res?.msg || `${action}失败`); + } + return res as T; +} + +/** 当前班主任班级信息 */ +export const getMyClassInfo = (): AxiosPromise => { + return request({ + url: '/classTeacherDorm/classTeacherDorm/myClass', + method: 'get' + }).then((res) => unwrapResponse(res, '获取班级信息')); +}; + +/** 本班宿舍违纪列表 */ +export const listClassTeacherViolations = ( + query?: ClassTeacherDormQuery +): AxiosPromise> => { + return request({ + url: '/classTeacherDorm/classTeacherDorm/violation/list', + method: 'get', + params: query + }).then((res) => unwrapResponse(res, '查询宿舍违纪')); +}; + +/** 本班宿舍点名列表 */ +export const listClassTeacherRollCalls = ( + query?: ClassTeacherDormQuery +): AxiosPromise> => { + return request({ + url: '/classTeacherDorm/classTeacherDorm/rollCall/list', + method: 'get', + params: query + }).then((res) => unwrapResponse(res, '查询宿舍点名')); +}; diff --git a/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.ts b/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.ts index c182c13c..e5c6b8a6 100644 --- a/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.ts +++ b/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.ts @@ -142,6 +142,17 @@ export const getStudentScoresByCheckId = (hygieneCheckId: string | number): Axio }); }; +/** + * 根据卫生检查ID查询违纪记录列表 + * @param hygieneCheckId 卫生检查ID + */ +export const getViolationsByCheckId = (hygieneCheckId: string | number) => { + return request({ + url: '/dormHygieneCheck/dormHygieneCheck/getViolations/' + hygieneCheckId, + method: 'get' + }); +}; + /** * 批量删除指定卫生检查的学生评分记录 * @param hygieneCheckId 卫生检查ID diff --git a/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types.ts b/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types.ts index 10eab976..ea07e5ac 100644 --- a/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types.ts +++ b/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types.ts @@ -113,6 +113,32 @@ export interface DormHygieneCheckVO { * 检查备注 */ checkRemarks: string; + + /** + * 违纪类型ID(兼容旧数据,展示用) + */ + disciplineTypeId?: string | number; + + /** + * 违纪类型名称(多项以、拼接) + */ + disciplineTypeName?: string; + + /** + * 扣分明细 + */ + deductionItems?: HygieneDeductionItem[]; +} + +export interface HygieneDeductionItem { + id?: string | number; + disciplineTypeId: string | number; + disciplineTypeName?: string; + score?: number; + minScore?: number; + maxScore?: number; + sortOrder?: number; + remark?: string; } export interface DormHygieneCheckForm extends BaseEntity { @@ -230,6 +256,21 @@ export interface DormHygieneCheckForm extends BaseEntity { * 检查备注 */ checkRemarks?: string; + + /** + * 违纪类型ID(兼容) + */ + disciplineTypeId?: string | number; + + /** + * 违纪类型名称(兼容) + */ + disciplineTypeName?: string; + + /** + * 扣分明细 + */ + deductionItems?: HygieneDeductionItem[]; } export interface DormHygieneCheckQuery extends PageQuery { @@ -639,4 +680,9 @@ export interface DormHygieneCheckCompleteForm extends DormHygieneCheckForm { * 学生评分数据列表 */ studentScores?: DormHygieneStudentScoreForm[]; + + /** + * 扣分明细 + */ + deductionItems?: HygieneDeductionItem[]; } diff --git a/web/src/views/xg/xgss/classTeacherDorm/index.vue b/web/src/views/xg/xgss/classTeacherDorm/index.vue new file mode 100644 index 00000000..29f1780e --- /dev/null +++ b/web/src/views/xg/xgss/classTeacherDorm/index.vue @@ -0,0 +1,401 @@ + + + + + diff --git a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckDialog.vue b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckDialog.vue index e03d4962..5c797f48 100644 --- a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckDialog.vue +++ b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckDialog.vue @@ -201,8 +201,8 @@ @@ -356,6 +356,7 @@ import { getToken } from '@/utils/auth'; import ImagePreview from '@/components/ImagePreview/index.vue'; import request from '@/utils/request'; import { DormHygieneCheckForm, DormHygieneCheckCompleteForm } from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types'; +import { isFemaleGender, isMaleGender, mergeGenderFromRoomStudents, normalizeGender } from './genderUtils'; // 暂时注释掉用户信息获取,避免linter错误 // import { getInfo } from '@/api/login'; @@ -1035,7 +1036,7 @@ const loadRoomStudents = async () => { console.log('获取到历史学生评分数据:', studentScoresRes.data); // 将历史评分数据转换为表格显示格式 - roomStudents.value = studentScoresRes.data.map((studentScore: any) => ({ + let mappedStudents = studentScoresRes.data.map((studentScore: any) => ({ // 保存学生评分记录的ID,用于编辑时提交 id: studentScore.id, studentId: studentScore.studentId, @@ -1047,7 +1048,7 @@ const loadRoomStudents = async () => { bedId: studentScore.bedId, bedName: studentScore.bedCode, // 使用bedCode作为bedName显示 bedCode: studentScore.bedCode, - gender: studentScore.gender || '1', // 默认女性,如果没有性别字段 + gender: normalizeGender(studentScore.gender), // 使用历史评分数据 student_score: studentScore.studentScore || null, attendance_status: studentScore.attendanceStatus || '1', @@ -1057,6 +1058,20 @@ const loadRoomStudents = async () => { improvementNotes: studentScore.improvementNotes || '' })); + const missingGender = mappedStudents.some((student: any) => !student.gender); + if (missingGender && form.roomId) { + const roomRes = await request({ + url: '/dormBedStudent/dormBedStudent/getRoomStudents', + method: 'get', + params: { roomId: form.roomId } + }); + if (roomRes.code === 200 && roomRes.data) { + mappedStudents = mergeGenderFromRoomStudents(mappedStudents, roomRes.data); + } + } + + roomStudents.value = mappedStudents; + console.log('转换后的学生数据:', roomStudents.value); return; } else { @@ -1076,6 +1091,7 @@ const loadRoomStudents = async () => { // 为每个学生设置默认值 roomStudents.value = res.data.map((student: any) => ({ ...student, + gender: normalizeGender(student.gender), student_score: 0, // 个人得分,新增时默认为0 attendance_status: student.attendance_status || '1', // 出勤状态,默认1-在寝 student_grade: student.student_grade || 'A' // 学生评级,默认A-优秀 diff --git a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckEditDialog.vue b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckEditDialog.vue index edc8c921..523205a5 100644 --- a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckEditDialog.vue +++ b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckEditDialog.vue @@ -47,30 +47,16 @@ - - - - - - - -
- - 类型说明:{{ selectedDisciplineTypeRemark }} -
-
+ + +
+ + @@ -193,8 +179,8 @@

{{ student.studentName }}

- 男 - 女 + 男 + 女

{{ student.studentNo }}

@@ -368,6 +354,11 @@ import request from '@/utils/request'; import ViolationRecordDialog from './components/ViolationRecordDialog.vue'; // @ts-expect-error - Component import without type declarations import LeaveProcessDialog from './components/LeaveProcessDialog.vue'; +import DisciplineDeductionMultiSelect from './components/DisciplineDeductionMultiSelect.vue'; +import type { HygieneDeductionItem } from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types'; +import { isFemaleGender, isMaleGender, mergeGenderFromRoomStudents, normalizeGender } from './genderUtils'; +import { normalizeStudentId, normalizeId } from './studentIdUtils'; +import { calcStudentViolationScoreTotal } from './violationScoreUtils'; // 组件实例 const { proxy } = getCurrentInstance() as ComponentInternalInstance; @@ -396,6 +387,7 @@ const studentsLoading = ref(false); const violationsLoading = ref(false); const attachmentList = ref([]); const disciplineTypeOptions = ref([]); +const deductionItems = ref([]); // 宿舍卫生检查表单 const hygieneForm = reactive({ @@ -449,8 +441,8 @@ const hygieneRules = reactive({ // 学生评分数据 const studentScores = ref([]); -// 违纪记录数据 -const violationRecords = ref>(new Map()); +// 违纪记录数据(使用普通对象,避免 Map 变更无法触发 Vue 响应式更新) +const violationRecords = ref>({}); // 学生请假数据 const studentLeaves = ref>(new Map()); @@ -491,14 +483,10 @@ const violationDialogTitle = computed(() => { return currentStudent.value ? `${currentStudent.value.studentName} - 违纪记录录入` : '违纪记录录入'; }); -// 获取违纪类型说明 -const selectedDisciplineTypeRemark = computed(() => { - if (hygieneForm.disciplineTypeId) { - const selectedType = disciplineTypeOptions.value.find((item) => item.id === hygieneForm.disciplineTypeId); - return selectedType ? selectedType.remark : ''; - } - return ''; -}); +// 扣分明细合计同步到宿舍扣分 +const handleDeductionTotalChange = (total: number) => { + hygieneForm.totalScore = total; +}; // 上传配置 const uploadAction = computed(() => { @@ -558,24 +546,26 @@ const loadDisciplineTypes = async () => { } }; -// 处理违纪类型变化 -const handleDisciplineTypeChange = () => { - if (hygieneForm.disciplineTypeId) { - const selectedType = disciplineTypeOptions.value.find((item) => item.id === hygieneForm.disciplineTypeId); - if (selectedType) { - hygieneForm.disciplineTypeName = selectedType.typeName; - // 自动填入最低分 - if (selectedType.minScore !== null && selectedType.minScore !== undefined) { - const minScore = parseFloat(selectedType.minScore); - if (!isNaN(minScore)) { - hygieneForm.totalScore = minScore; - ElMessage.success(`已自动填入最低扣分:${minScore}分`); - } +// 初始化扣分明细 +const initDeductionItems = (data?: any) => { + if (data?.deductionItems && data.deductionItems.length > 0) { + deductionItems.value = data.deductionItems.map((item: HygieneDeductionItem, index: number) => ({ + ...item, + sortOrder: item.sortOrder ?? index + 1 + })); + } else if (data?.disciplineTypeId) { + deductionItems.value = [ + { + disciplineTypeId: data.disciplineTypeId, + disciplineTypeName: data.disciplineTypeName, + score: data.totalScore ?? 0, + sortOrder: 1 } - } + ]; } else { - hygieneForm.disciplineTypeName = ''; + deductionItems.value = []; } + hygieneForm.totalScore = deductionItems.value.reduce((sum, item) => sum + (Number(item.score) || 0), 0); }; // 初始化表单数据(已简化,主要逻辑移到watch中) @@ -594,32 +584,48 @@ const initFormData = async () => { // 加载学生评分数据 const loadStudentScores = async () => { - console.log('loadStudentScores 方法被调用,hygieneForm.id:', hygieneForm.id); - - if (!hygieneForm.id) { + const checkId = normalizeId(hygieneForm.id); + if (!checkId) { console.log('没有检查ID,跳过学生数据加载'); return; } - console.log('开始加载学生评分数据,检查ID:', hygieneForm.id); + console.log('开始加载学生评分数据,检查ID:', checkId); studentsLoading.value = true; try { const res = await request({ - url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${hygieneForm.id}`, + url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${checkId}`, method: 'get' }); - console.log('学生评分数据API响应:', res); + if (res.code === 200 && Array.isArray(res.data)) { + let scores = res.data.map((student: any) => ({ + ...student, + studentId: normalizeStudentId(student.studentId), + gender: normalizeGender(student.gender) + })); - if (res.code === 200 && res.data) { - studentScores.value = res.data; - console.log('学生评分数据加载成功:', res.data.length, '条'); + const missingGender = scores.some((student: any) => !student.gender); + if (missingGender && hygieneForm.roomId) { + try { + const roomRes = await request({ + url: '/dormBedStudent/dormBedStudent/getRoomStudents', + method: 'get', + params: { roomId: hygieneForm.roomId } + }); + if (roomRes.code === 200 && roomRes.data) { + scores = mergeGenderFromRoomStudents(scores, roomRes.data); + } + } catch (error) { + console.warn('补全学生性别失败,将仅展示已关联到的性别信息', error); + } + } - // 同时加载违纪记录和请假信息 - await Promise.all([loadViolationRecords(), loadStudentLeaves()]); + studentScores.value = scores; + console.log('学生评分数据加载成功:', scores.length, '条'); + await loadStudentLeaves(); } else { studentScores.value = []; - console.log('学生评分数据为空,响应码:', res.code); } } catch (error) { console.error('加载学生评分数据失败:', error); @@ -631,29 +637,55 @@ const loadStudentScores = async () => { }; // 加载违纪记录数据 -const loadViolationRecords = async () => { - if (!hygieneForm.id) return; +const applyViolationList = (list: any[]) => { + const grouped: Record = {}; + (list || []).forEach((violation: any) => { + const studentId = normalizeStudentId(violation.studentId); + if (!studentId) return; + if (!grouped[studentId]) { + grouped[studentId] = []; + } + grouped[studentId].push({ + ...violation, + studentId, + disciplineRecordId: + violation.disciplineRecordId != null ? normalizeId(violation.disciplineRecordId) : undefined + }); + }); + violationRecords.value = grouped; + if (studentScores.value.length > 0) { + syncStudentScoreFromViolations(); + } +}; +const loadViolationRecords = async (prefetched?: any[] | null) => { + if (prefetched !== undefined) { + applyViolationList(prefetched || []); + console.log('违纪记录(预加载):', (prefetched || []).length, '条'); + return; + } + + const checkId = normalizeId(hygieneForm.id || props.formData?.id); + if (!checkId) return; + + violationsLoading.value = true; try { const res = await request({ - url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${hygieneForm.id}`, + url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${checkId}`, method: 'get' }); - if (res.code === 200 && res.data) { - // 重新组织违纪记录数据,按学生ID分组 - violationRecords.value.clear(); - res.data.forEach((violation: any) => { - const studentId = violation.studentId; - if (!violationRecords.value.has(studentId)) { - violationRecords.value.set(studentId, []); - } - violationRecords.value.get(studentId)?.push(violation); - }); + if (res.code === 200 && Array.isArray(res.data)) { + applyViolationList(res.data); + console.log('违纪记录加载成功:', res.data.length, '条'); + } else { + violationRecords.value = {}; } } catch (error) { console.error('加载违纪记录数据失败:', error); ElMessage.error('加载违纪记录数据失败'); + } finally { + violationsLoading.value = false; } }; @@ -675,9 +707,9 @@ const loadStudentLeaves = async () => { if (res.code === 200 && res.data) { studentLeaves.value.clear(); Object.keys(res.data).forEach((studentId) => { - studentLeaves.value.set(Number(studentId), res.data[studentId]); - // 如果学生有请假信息,自动设置出勤状态为3(请假) - const student = studentScores.value.find((s) => s.studentId === Number(studentId)); + const normalizedId = normalizeStudentId(studentId); + studentLeaves.value.set(normalizedId, res.data[studentId]); + const student = studentScores.value.find((s) => normalizeStudentId(s.studentId) === normalizedId); if (student) { student.attendanceStatus = '3'; } @@ -691,12 +723,31 @@ const loadStudentLeaves = async () => { // 获取学生的违纪记录 const getStudentViolations = (studentId: string | number) => { - return violationRecords.value.get(studentId) || []; + return violationRecords.value[normalizeStudentId(studentId)] || []; +}; + +/** 根据违纪记录合计同步个人扣分 */ +const syncStudentScoreFromViolations = (studentId?: string | number) => { + const syncOne = (sid: string) => { + const student = studentScores.value.find((item) => normalizeStudentId(item.studentId) === sid); + if (student) { + student.studentScore = calcStudentViolationScoreTotal(getStudentViolations(sid)); + } + }; + + if (studentId != null && studentId !== '') { + syncOne(normalizeStudentId(studentId)); + return; + } + + studentScores.value.forEach((student) => { + syncOne(normalizeStudentId(student.studentId)); + }); }; // 获取学生的请假信息 const getStudentLeave = (studentId: string | number) => { - return studentLeaves.value.get(studentId) || null; + return studentLeaves.value.get(normalizeStudentId(studentId)) || null; }; // 处理附件数据 @@ -829,7 +880,8 @@ const editViolationRecord = (student: any, violation: any, index: number) => { // 删除违纪记录 const removeViolationRecord = async (studentId: string | number, index: number) => { - const violations = violationRecords.value.get(studentId) || []; + const normalizedId = normalizeStudentId(studentId); + const violations = [...(violationRecords.value[normalizedId] || [])]; const violation = violations[index]; // 检查是否为已撤销的违纪记录 @@ -851,11 +903,14 @@ const removeViolationRecord = async (studentId: string | number, index: number) violations.splice(index, 1); + const nextRecords = { ...violationRecords.value }; if (violations.length === 0) { - violationRecords.value.delete(studentId); + delete nextRecords[normalizedId]; } else { - violationRecords.value.set(studentId, violations); + nextRecords[normalizedId] = violations; } + violationRecords.value = nextRecords; + syncStudentScoreFromViolations(normalizedId); if (window.ElMessage) { window.ElMessage.success('违纪记录删除成功'); @@ -867,23 +922,32 @@ const removeViolationRecord = async (studentId: string | number, index: number) // 违纪记录确认 const handleViolationConfirm = (violationData: any) => { - const studentId = currentStudent.value.studentId; - const violations = violationRecords.value.get(studentId) || []; + const studentId = normalizeStudentId(currentStudent.value.studentId); + const violations = [...(violationRecords.value[studentId] || [])]; - if (currentViolationIndex.value >= 0) { - // 编辑模式 + if (Array.isArray(violationData)) { + violations.push(...violationData); + if (window.ElMessage) { + window.ElMessage.success(`已添加 ${violationData.length} 条违纪记录`); + } + } else if (currentViolationIndex.value >= 0) { violations[currentViolationIndex.value] = violationData; + if (window.ElMessage) { + window.ElMessage.success('违纪记录保存成功'); + } } else { - // 新增模式 violations.push(violationData); + if (window.ElMessage) { + window.ElMessage.success('违纪记录保存成功'); + } } - violationRecords.value.set(studentId, violations); + violationRecords.value = { + ...violationRecords.value, + [studentId]: violations + }; + syncStudentScoreFromViolations(studentId); violationDialogVisible.value = false; - - if (window.ElMessage) { - window.ElMessage.success('违纪记录保存成功'); - } }; // 违纪记录弹窗关闭 @@ -1110,8 +1174,16 @@ const submitForm = async () => { try { await hygieneFormRef.value?.validate(); + if ((hygieneForm.totalScore || 0) > 100) { + ElMessage.error('宿舍合计扣分不能超过100分'); + return; + } + buttonLoading.value = true; + // 提交前同步个人扣分为违纪合计 + syncStudentScoreFromViolations(); + // 处理附件数据 const attachmentOssIds = attachmentList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId); hygieneForm.attachmentIds = attachmentOssIds.join(','); @@ -1138,12 +1210,30 @@ const submitForm = async () => { // 处理违纪记录数据 const violationData: any[] = []; - violationRecords.value.forEach((violations, studentId) => { + Object.entries(violationRecords.value).forEach(([studentId, violations]) => { + const student = studentScores.value.find((item) => normalizeStudentId(item.studentId) === studentId); violations.forEach((violation) => { violationData.push({ - ...violation, - studentId, - violationSource: '1' // 宿舍卫生检查来源 + disciplineRecordId: + violation.disciplineRecordId != null ? normalizeId(violation.disciplineRecordId) : undefined, + studentId: normalizeStudentId(student?.studentId || violation.studentId || studentId), + studentNo: violation.studentNo || student?.studentNo || '', + studentName: violation.studentName || student?.studentName || '', + classId: violation.classId ?? student?.classId, + className: violation.className || student?.className || '', + majorId: violation.majorId ?? student?.majorId, + majorName: violation.majorName || student?.majorName || '', + collegeId: violation.collegeId ?? student?.collegeId, + collegeName: violation.collegeName || student?.collegeName || '', + disciplineTypeId: violation.disciplineTypeId, + disciplineTypeName: violation.disciplineTypeName, + disciplineDate: violation.disciplineDate, + disciplineReason: violation.disciplineReason, + disciplineContent: violation.disciplineContent, + scoreDeduction: violation.scoreDeduction, + status: violation.status || '1', + attachmentOssIds: violation.attachmentOssIds || '', + violationSource: '1' }); }); }); @@ -1151,6 +1241,7 @@ const submitForm = async () => { // 构建提交数据 const submitData = { ...hygieneForm, + deductionItems: deductionItems.value, studentScores: studentScoresData, violationData }; @@ -1171,44 +1262,67 @@ const handleClose = () => { emit('close'); }; -// 监听弹窗显示状态 -watch( - () => props.visible, - async (visible, oldVisible) => { - console.log('弹窗显示状态变化:', oldVisible, '->', visible); - if (visible) { - console.log('弹窗打开,开始初始化表单数据'); - // 只加载违纪类型数据,其他数据通过formData的watch来加载 - await loadDisciplineTypes(); - } else { - console.log('弹窗关闭'); - } +const detailLoadToken = ref(0); + +const syncFormFromProps = (data?: any) => { + if (!data) return; + Object.assign(hygieneForm, { + ...data, + id: data.id != null ? normalizeId(data.id) : undefined, + roomId: data.roomId != null ? normalizeId(data.roomId) : undefined + }); + initDeductionItems(data); +}; + +const loadCheckDetailData = async () => { + const checkId = normalizeId(props.formData?.id); + if (!props.visible || !checkId) { + return; } -); -// 监听表单数据变化 + const token = ++detailLoadToken.value; + syncFormFromProps(props.formData); + hygieneForm.id = checkId; + + const hasPrefetchedViolations = + props.formData != null && Object.prototype.hasOwnProperty.call(props.formData, '_prefetchedViolations'); + + console.log('开始加载检查详情,checkId:', checkId); + try { + await loadStudentScores(); + if (token !== detailLoadToken.value) return; + + await loadViolationRecords(hasPrefetchedViolations ? props.formData._prefetchedViolations || [] : undefined); + if (token !== detailLoadToken.value) return; + + syncStudentScoreFromViolations(); + + await handleAttachmentData(); + console.log('检查详情加载完成'); + } catch (error) { + console.error('加载检查详情失败:', error); + } +}; + +// 弹窗打开 / 检查ID变化时加载详情(合并 visible + formData.id,避免竞态) watch( - () => props.formData, - async (newData, oldData) => { - console.log('props.formData 变化:', oldData, '->', newData); - if (newData) { - console.log('更新表单数据'); - Object.assign(hygieneForm, newData); - console.log('表单数据更新后,hygieneForm.id:', hygieneForm.id); - - // 如果弹窗是打开状态且有ID,需要重新加载相关数据 - if (props.visible && hygieneForm.id) { - console.log('检测到有检查ID,重新加载学生数据和附件数据...'); - try { - await Promise.all([loadStudentScores(), handleAttachmentData()]); - console.log('学生数据和附件数据重新加载完成'); - } catch (error) { - console.error('重新加载数据失败:', error); - } - } + () => [props.visible, normalizeId(props.formData?.id)] as const, + async ([visible, checkId]) => { + if (!visible) { + detailLoadToken.value += 1; + violationRecords.value = {}; + studentScores.value = []; + hygieneForm.id = undefined; + return; } + + if (!checkId) return; + + await loadDisciplineTypes(); + await nextTick(); + await loadCheckDetailData(); }, - { deep: true, immediate: true } + { immediate: true } ); diff --git a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckMainDialog.vue b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckMainDialog.vue index 78da6572..83b2e67c 100644 --- a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckMainDialog.vue +++ b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckMainDialog.vue @@ -201,12 +201,13 @@ + + diff --git a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/components/ViolationRecordDialog.vue b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/components/ViolationRecordDialog.vue index 6090f790..f3436d5e 100644 --- a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/components/ViolationRecordDialog.vue +++ b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/components/ViolationRecordDialog.vue @@ -11,8 +11,8 @@ - - + + -
类型说明:{{ selectedDisciplineTypeRemark }}
+ + + @@ -53,12 +110,8 @@
- + - -
@@ -186,7 +239,7 @@ interface Props { // 组件事件 interface Emits { (e: 'update:modelValue', value: boolean): void; - (e: 'confirm', data: any): void; + (e: 'confirm', data: any | any[]): void; (e: 'close'): void; } @@ -207,6 +260,15 @@ const attachmentList = ref([]); // 所有违纪类型列表 const allViolationTypes = ref([]); +interface ViolationItem { + disciplineTypeId: string | number; + disciplineTypeName: string; + scoreDeduction: number; +} + +const selectedTypeIds = ref>([]); +const violationItems = ref([]); + // 当前选择的违纪类型扣分范围 const currentTypeMinScore = ref(0); const currentTypeMaxScore = ref(100); @@ -236,8 +298,7 @@ const form = reactive({ const rules = reactive({ disciplineTypeId: [{ required: true, message: '请选择违纪类型', trigger: 'change' }], disciplineDate: [{ required: true, message: '请选择违纪日期', trigger: 'change' }], - scoreDeduction: [{ required: true, message: '请输入扣分分值', trigger: 'blur' }], - disciplineReason: [{ required: true, message: '请输入违纪原因', trigger: 'blur' }] + scoreDeduction: [{ required: true, message: '请输入扣分分值', trigger: 'blur' }] }); // 计算属性 @@ -248,6 +309,13 @@ const dialogVisible = computed({ const dialogTitle = computed(() => props.title); +const isEditMode = computed(() => !!props.violationData); + +const violationItemsTotalScore = computed(() => { + const sum = violationItems.value.reduce((acc, item) => acc + (Number(item.scoreDeduction) || 0), 0); + return Number(sum.toFixed(2)); +}); + // 上传配置 const uploadAction = computed(() => { return import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'; @@ -314,6 +382,67 @@ const getAllViolationTypes = async () => { } }; +const getTypeOption = (typeId?: string | number) => + allViolationTypes.value.find((item) => String(item.id) === String(typeId)); + +const parseScore = (value: unknown, fallback = 0) => { + if (value === null || value === undefined) return fallback; + return typeof value === 'number' ? value : parseFloat(String(value)) || fallback; +}; + +const getItemMinScore = (item: ViolationItem) => parseScore(getTypeOption(item.disciplineTypeId)?.minScore, 0); + +const getItemMaxScore = (item: ViolationItem) => parseScore(getTypeOption(item.disciplineTypeId)?.maxScore, 100); + +const getItemRemark = (typeId?: string | number) => getTypeOption(typeId)?.remark || ''; + +const handleTypeSelectionChange = (typeIds: Array) => { + const currentMap = new Map(violationItems.value.map((item) => [String(item.disciplineTypeId), item])); + const nextItems: ViolationItem[] = []; + + typeIds.forEach((typeId) => { + const option = getTypeOption(typeId); + if (!option) return; + const existing = currentMap.get(String(typeId)); + nextItems.push({ + disciplineTypeId: option.id, + disciplineTypeName: option.typeName, + scoreDeduction: existing?.scoreDeduction ?? parseScore(option.minScore, 0) + }); + }); + + violationItems.value = nextItems; +}; + +const removeViolationItem = (index: number) => { + violationItems.value.splice(index, 1); + selectedTypeIds.value = violationItems.value.map((item) => item.disciplineTypeId); +}; + +const buildSharedViolationFields = () => { + const attachmentOssIds = attachmentList.value + .filter((file) => file.status === 'success' && file.ossId) + .map((file) => file.ossId) + .join(','); + + return { + studentId: form.studentId, + studentNo: form.studentNo, + studentName: form.studentName, + collegeId: form.collegeId, + collegeName: form.collegeName, + majorId: form.majorId, + majorName: form.majorName, + classId: form.classId, + className: form.className, + disciplineDate: form.disciplineDate, + disciplineReason: form.disciplineReason, + disciplineContent: form.disciplineContent, + attachmentOssIds, + violationSource: form.violationSource || '1' + }; +}; + // 违纪类型变化处理 const handleDisciplineTypeChange = (typeId: string | number) => { const selectedType = allViolationTypes.value.find((item) => item.id === typeId); @@ -414,8 +543,9 @@ const initForm = () => { } } else { console.log('违纪记录:新增模式,重置附件列表'); - // 重置附件列表 attachmentList.value = []; + selectedTypeIds.value = []; + violationItems.value = []; } }; @@ -695,15 +825,49 @@ const handleDownloadAttachment = async (file: any) => { // 确认提交 const handleConfirm = async () => { try { - await violationFormRef.value?.validate(); - buttonLoading.value = true; - // 处理附件数据 - const attachmentOssIds = attachmentList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId); - form.attachmentOssIds = attachmentOssIds.join(','); + if (isEditMode.value) { + await violationFormRef.value?.validate(); + const attachmentOssIds = attachmentList.value + .filter((file) => file.status === 'success' && file.ossId) + .map((file) => file.ossId); + form.attachmentOssIds = attachmentOssIds.join(','); + emit('confirm', { ...form }); + return; + } - emit('confirm', { ...form }); + if (!form.disciplineDate) { + ElMessage.warning('请选择违纪日期'); + return; + } + if (violationItems.value.length === 0) { + ElMessage.warning('请至少选择一个违纪类型'); + return; + } + + for (const item of violationItems.value) { + const min = getItemMinScore(item); + const max = getItemMaxScore(item); + const score = Number(item.scoreDeduction); + if (Number.isNaN(score)) { + ElMessage.warning(`请填写「${item.disciplineTypeName}」的扣分分值`); + return; + } + if (score < min || score > max) { + ElMessage.warning(`「${item.disciplineTypeName}」扣分须在 ${min}-${max} 分之间`); + return; + } + } + + const sharedFields = buildSharedViolationFields(); + const records = violationItems.value.map((item) => ({ + ...sharedFields, + disciplineTypeId: item.disciplineTypeId, + disciplineTypeName: item.disciplineTypeName, + scoreDeduction: item.scoreDeduction + })); + emit('confirm', records); } catch (error) { console.error('提交失败:', error); ElMessage.error('请检查表单填写是否完整'); @@ -729,6 +893,8 @@ const handleClose = () => { attachmentList.value = []; currentTypeMinScore.value = 0; currentTypeMaxScore.value = 100; + selectedTypeIds.value = []; + violationItems.value = []; }; // 重置表单 @@ -775,6 +941,8 @@ const resetForm = () => { attachmentList.value = []; currentTypeMinScore.value = 0; currentTypeMaxScore.value = 100; + selectedTypeIds.value = []; + violationItems.value = []; }; // 获取违纪类型说明 @@ -898,6 +1066,46 @@ onMounted(() => { line-height: 1.4; } +.violation-item-list { + margin: 0 0 16px 120px; + padding: 12px; + background: #f8f9fb; + border-radius: 8px; +} + +.violation-item-row + .violation-item-row { + margin-top: 10px; + padding-top: 10px; + border-top: 1px dashed #e4e7ed; +} + +.violation-item-main { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.violation-item-name { + min-width: 120px; + font-weight: 500; + color: #303133; +} + +.violation-item-total { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #e4e7ed; + font-size: 13px; + color: #606266; +} + +.inline-remark { + margin-left: 0; + padding-left: 0; + margin-top: 6px; +} + /* 区域标签样式 */ .area-tag { display: inline-block; diff --git a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/genderUtils.ts b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/genderUtils.ts new file mode 100644 index 00000000..dcf83408 --- /dev/null +++ b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/genderUtils.ts @@ -0,0 +1,58 @@ +/** 归一化性别值:0=男,1=女 */ +export function normalizeGender(gender: unknown): string | undefined { + if (gender === null || gender === undefined || gender === '') { + return undefined; + } + const value = String(gender); + if (value === '0' || value === '男') { + return '0'; + } + if (value === '1' || value === '女') { + return '1'; + } + return value; +} + +export function isMaleGender(gender: unknown): boolean { + return normalizeGender(gender) === '0'; +} + +export function isFemaleGender(gender: unknown): boolean { + return normalizeGender(gender) === '1'; +} + +/** 用房间在住学生信息补全缺失的性别 */ +export function mergeGenderFromRoomStudents( + students: T[], + roomStudents: Array<{ studentId?: unknown; studentNo?: string; gender?: unknown }> +): T[] { + if (!students.length || !roomStudents.length) { + return students; + } + + const genderByStudentId = new Map(); + const genderByStudentNo = new Map(); + roomStudents.forEach((student) => { + const gender = normalizeGender(student.gender); + if (!gender) { + return; + } + if (student.studentId !== undefined && student.studentId !== null) { + genderByStudentId.set(String(student.studentId), gender); + } + if (student.studentNo) { + genderByStudentNo.set(student.studentNo, gender); + } + }); + + return students.map((student) => { + if (normalizeGender(student.gender)) { + return { ...student, gender: normalizeGender(student.gender) }; + } + const gender = + (student.studentId !== undefined && student.studentId !== null + ? genderByStudentId.get(String(student.studentId)) + : undefined) || (student.studentNo ? genderByStudentNo.get(student.studentNo) : undefined); + return gender ? { ...student, gender } : student; + }); +} diff --git a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.vue b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.vue index fe402d14..45356000 100644 --- a/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.vue +++ b/web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.vue @@ -107,6 +107,7 @@ +