This commit is contained in:
zlwu committed 2026-06-26 15:30:27 +08:00
commit 1155a976b9
49 files changed
+3227 -424

No files matched your search

@@ -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<ClassTeacherClassInfoVo> myClass() {
return R.ok(classTeacherDormViewService.getMyClassInfo());
}
/**
* 本班宿舍违纪记录(卫生检查 + 宿舍违纪登记)
*/
@SaCheckPermission("classTeacherDorm:classTeacherDorm:query")
@GetMapping("/violation/list")
public TableDataInfo<ClassTeacherDormViolationVo> violationList(ClassTeacherDormQueryBo bo, PageQuery pageQuery) {
return classTeacherDormViewService.queryViolationPage(bo, pageQuery);
}
/**
* 本班宿舍点名明细
*/
@SaCheckPermission("classTeacherDorm:classTeacherDorm:query")
@GetMapping("/rollCall/list")
public TableDataInfo<ClassTeacherDormRollCallVo> rollCallList(ClassTeacherDormQueryBo bo, PageQuery pageQuery) {
return classTeacherDormViewService.queryRollCallPage(bo, pageQuery);
}
}
@@ -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;
}
@@ -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;
}
@@ -7,6 +7,7 @@ import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data; import lombok.Data;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.dromara.xg.domain.bo.XgDormHygieneCheckDeductionBo;
import java.util.List; import java.util.List;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
@@ -159,6 +160,11 @@ public class XgDormHygieneCheckCompleteBo {
*/ */
private String disciplineTypeName; private String disciplineTypeName;
/**
* 扣分明细列表
*/
private List<XgDormHygieneCheckDeductionBo> deductionItems;
/** /**
* 学生评分数据列表 * 学生评分数据列表
*/ */
@@ -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;
}
@@ -95,7 +95,6 @@ public class XgViolationDataBo {
/** /**
* 违纪原因 * 违纪原因
*/ */
@NotBlank(message = "违纪原因不能为空")
private String disciplineReason; private String disciplineReason;
/** /**
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -90,6 +90,12 @@ public class DormHygieneCheckExportVo implements Serializable {
@ExcelProperty(value = "宿舍总分") @ExcelProperty(value = "宿舍总分")
private Double totalScore; private Double totalScore;
/**
* 扣分项明细
*/
@ExcelProperty(value = "扣分项")
private String disciplineTypeName;
/** /**
* 卫生等级 * 卫生等级
*/ */
@@ -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;
}
@@ -13,6 +13,8 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import java.util.List;
import org.dromara.xg.domain.vo.XgDormHygieneCheckDeductionVo;
/** /**
* 宿舍卫生检查视图对象 xg_dorm_hygiene_check * 宿舍卫生检查视图对象 xg_dorm_hygiene_check
@@ -183,4 +185,9 @@ public class XgDormHygieneCheckVo implements Serializable {
@ExcelProperty(value = "违纪类型名称") @ExcelProperty(value = "违纪类型名称")
private String disciplineTypeName; private String disciplineTypeName;
/**
* 扣分明细列表
*/
private List<XgDormHygieneCheckDeductionVo> deductionItems;
} }
@@ -113,6 +113,11 @@ public class XgDormHygieneStudentScoreVo implements Serializable {
@ExcelProperty(value = "床位编号") @ExcelProperty(value = "床位编号")
private String bedCode; private String bedCode;
/**
* 性别(0男 1女,来自xg_student_msg,查询时关联填充)
*/
private String gender;
/** /**
* 学生个人得分(0-100分) * 学生个人得分(0-100分)
*/ */
@@ -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<ClassTeacherDormViolationVo> selectViolationPage(
Page<ClassTeacherDormViolationVo> page,
@Param("classId") Long classId,
@Param("bo") ClassTeacherDormQueryBo bo);
IPage<ClassTeacherDormRollCallVo> selectRollCallPage(
Page<ClassTeacherDormRollCallVo> page,
@Param("classId") Long classId,
@Param("bo") ClassTeacherDormQueryBo bo);
}
@@ -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<XgDormHygieneCheckDeduction, XgDormHygieneCheckDeductionVo> {
List<XgDormHygieneCheckDeductionVo> selectVoListByHygieneCheckId(@Param("hygieneCheckId") Long hygieneCheckId);
}
@@ -4,6 +4,10 @@ import org.dromara.xg.domain.XgDormHygieneStudentScore;
import org.dromara.xg.domain.vo.XgDormHygieneStudentScoreVo; import org.dromara.xg.domain.vo.XgDormHygieneStudentScoreVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/** /**
* 宿舍卫生检查学生评分Mapper接口 * 宿舍卫生检查学生评分Mapper接口
* *
@@ -12,4 +16,12 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
*/ */
public interface XgDormHygieneStudentScoreMapper extends BaseMapperPlus<XgDormHygieneStudentScore, XgDormHygieneStudentScoreVo> { public interface XgDormHygieneStudentScoreMapper extends BaseMapperPlus<XgDormHygieneStudentScore, XgDormHygieneStudentScoreVo> {
/**
* 根据卫生检查ID查询学生评分列表(关联学生档案补充性别)
*
* @param hygieneCheckId 卫生检查ID
* @return 学生评分列表
*/
List<XgDormHygieneStudentScoreVo> selectVoListByHygieneCheckId(@Param("hygieneCheckId") Long hygieneCheckId);
} }
@@ -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<ClassTeacherDormViolationVo> queryViolationPage(ClassTeacherDormQueryBo bo, PageQuery pageQuery);
TableDataInfo<ClassTeacherDormRollCallVo> queryRollCallPage(ClassTeacherDormQueryBo bo, PageQuery pageQuery);
}
@@ -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<XgDormHygieneCheckDeductionVo> listByHygieneCheckId(Long hygieneCheckId);
void replaceByHygieneCheckId(Long hygieneCheckId, List<XgDormHygieneCheckDeductionBo> items);
}
@@ -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<ClassTeacherDormViolationVo> queryViolationPage(ClassTeacherDormQueryBo bo, PageQuery pageQuery) {
Long classId = resolveClassId();
normalizeDateRange(bo);
Page<ClassTeacherDormViolationVo> page = pageQuery.disableDefaultOrder().build();
IPage<ClassTeacherDormViolationVo> result = classTeacherDormViewMapper.selectViolationPage(page, classId, bo);
return TableDataInfo.build(result);
}
@Override
public TableDataInfo<ClassTeacherDormRollCallVo> queryRollCallPage(ClassTeacherDormQueryBo bo, PageQuery pageQuery) {
Long classId = resolveClassId();
normalizeDateRange(bo);
Page<ClassTeacherDormRollCallVo> page = pageQuery.disableDefaultOrder().build();
IPage<ClassTeacherDormRollCallVo> 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);
}
}
}
@@ -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<XgDormHygieneCheckDeductionVo> listByHygieneCheckId(Long hygieneCheckId) {
if (hygieneCheckId == null) {
return Collections.emptyList();
}
return baseMapper.selectVoListByHygieneCheckId(hygieneCheckId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void replaceByHygieneCheckId(Long hygieneCheckId, List<XgDormHygieneCheckDeductionBo> items) {
if (hygieneCheckId == null) {
return;
}
LambdaQueryWrapper<XgDormHygieneCheckDeduction> 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);
}
}
}
@@ -14,10 +14,12 @@ import org.springframework.transaction.annotation.Transactional;
import org.dromara.xg.domain.bo.XgDormHygieneCheckBo; import org.dromara.xg.domain.bo.XgDormHygieneCheckBo;
import org.dromara.xg.domain.bo.XgDormHygieneCheckCompleteBo; import org.dromara.xg.domain.bo.XgDormHygieneCheckCompleteBo;
import org.dromara.xg.domain.bo.XgDormHygieneStudentScoreBo; 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.XgHygieneCheckViolationBo;
import org.dromara.xg.domain.bo.XgViolationDataBo; import org.dromara.xg.domain.bo.XgViolationDataBo;
import org.dromara.xg.domain.vo.DormHygieneCheckExportVo; import org.dromara.xg.domain.vo.DormHygieneCheckExportVo;
import org.dromara.xg.domain.vo.XgDormHygieneCheckVo; 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.XgDormHygieneCheck;
import org.dromara.xg.domain.XgDormHygieneStudentScore; import org.dromara.xg.domain.XgDormHygieneStudentScore;
import org.dromara.xg.mapper.XgDormHygieneCheckMapper; 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.IXgStudentMsgService;
import org.dromara.xg.service.IXgHygieneCheckViolationService; import org.dromara.xg.service.IXgHygieneCheckViolationService;
import org.dromara.xg.service.IXgDormHygieneStudentScoreService; import org.dromara.xg.service.IXgDormHygieneStudentScoreService;
import org.dromara.xg.service.IXgDormHygieneCheckDeductionService;
import org.dromara.xg.mapper.XgHygieneCheckViolationMapper; import org.dromara.xg.mapper.XgHygieneCheckViolationMapper;
import org.dromara.system.service.ISysDictDataService; import org.dromara.system.service.ISysDictDataService;
import org.springframework.context.ApplicationContext; 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.vo.XgHygieneCheckViolationVo;
import org.dromara.xg.domain.XgHygieneCheckViolation; import org.dromara.xg.domain.XgHygieneCheckViolation;
import java.util.Collections; import java.util.Collections;
import java.util.ArrayList;
import org.dromara.common.core.exception.ServiceException;
import java.math.BigDecimal;
import java.math.RoundingMode;
/** /**
* 宿舍卫生检查Service业务层处理 * 宿舍卫生检查Service业务层处理
@@ -60,6 +67,7 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService
private final IXgHygieneCheckViolationService violationService; private final IXgHygieneCheckViolationService violationService;
private final IXgDormHygieneStudentScoreService studentScoreService; private final IXgDormHygieneStudentScoreService studentScoreService;
private final XgHygieneCheckViolationMapper violationMapper; private final XgHygieneCheckViolationMapper violationMapper;
private final IXgDormHygieneCheckDeductionService deductionService;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
/** /**
@@ -70,7 +78,21 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService
*/ */
@Override @Override
public XgDormHygieneCheckVo queryById(Long id) { public XgDormHygieneCheckVo queryById(Long id) {
return baseMapper.selectVoById(id); XgDormHygieneCheckVo vo = baseMapper.selectVoById(id);
if (vo == null) {
return null;
}
List<XgDormHygieneCheckDeductionVo> 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) @Transactional(rollbackFor = Exception.class)
public Long saveCompleteData(XgDormHygieneCheckCompleteBo bo) { public Long saveCompleteData(XgDormHygieneCheckCompleteBo bo) {
try { try {
List<XgDormHygieneCheckDeductionBo> deductionItems = resolveDeductionItems(bo);
// 1. 转换并保存主表数据 // 1. 转换并保存主表数据
XgDormHygieneCheck add = MapstructUtils.convert(bo, XgDormHygieneCheck.class); XgDormHygieneCheck add = MapstructUtils.convert(bo, XgDormHygieneCheck.class);
applyDeductionSummary(add, deductionItems);
validEntityBeforeSave(add); validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0; boolean flag = baseMapper.insert(add) > 0;
log.info("保存宿舍卫生检查主表数据: {}", add); log.info("保存宿舍卫生检查主表数据: {}", add);
@@ -657,6 +681,9 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService
Long hygieneCheckId = add.getId(); Long hygieneCheckId = add.getId();
// 1.1 保存扣分明细
deductionService.replaceByHygieneCheckId(hygieneCheckId, deductionItems);
// 2. 保存学生评分数据 // 2. 保存学生评分数据
if (bo.getStudentScores() != null && !bo.getStudentScores().isEmpty()) { if (bo.getStudentScores() != null && !bo.getStudentScores().isEmpty()) {
for (XgDormHygieneStudentScoreBo studentScoreBo : bo.getStudentScores()) { for (XgDormHygieneStudentScoreBo studentScoreBo : bo.getStudentScores()) {
@@ -901,8 +928,13 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService
setFieldValue(disciplineRecordBo, "disciplineDate", java.time.LocalDate.now()); 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()); setFieldValue(disciplineRecordBo, "disciplineContent", violationBo.getDisciplineContent());
// 设置扣分分值 // 设置扣分分值
@@ -1005,8 +1037,10 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Boolean updateCompleteData(XgDormHygieneCheckCompleteBo bo) { public Boolean updateCompleteData(XgDormHygieneCheckCompleteBo bo) {
try { try {
List<XgDormHygieneCheckDeductionBo> deductionItems = resolveDeductionItems(bo);
// 1. 更新主表数据 // 1. 更新主表数据
XgDormHygieneCheck update = MapstructUtils.convert(bo, XgDormHygieneCheck.class); XgDormHygieneCheck update = MapstructUtils.convert(bo, XgDormHygieneCheck.class);
applyDeductionSummary(update, deductionItems);
validEntityBeforeSave(update); validEntityBeforeSave(update);
boolean flag = baseMapper.updateById(update) > 0; boolean flag = baseMapper.updateById(update) > 0;
log.info("更新宿舍卫生检查主表数据: {}", update); log.info("更新宿舍卫生检查主表数据: {}", update);
@@ -1018,6 +1052,9 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService
Long hygieneCheckId = update.getId(); Long hygieneCheckId = update.getId();
// 1.1 更新扣分明细
deductionService.replaceByHygieneCheckId(hygieneCheckId, deductionItems);
// 2. 更新学生评分数据 - 先删除旧数据,再插入新数据 // 2. 更新学生评分数据 - 先删除旧数据,再插入新数据
LambdaQueryWrapper<XgDormHygieneStudentScore> scoreWrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<XgDormHygieneStudentScore> scoreWrapper = Wrappers.lambdaQuery();
scoreWrapper.eq(XgDormHygieneStudentScore::getHygieneCheckId, hygieneCheckId); scoreWrapper.eq(XgDormHygieneStudentScore::getHygieneCheckId, hygieneCheckId);
@@ -1341,4 +1378,63 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService
} }
} }
private List<XgDormHygieneCheckDeductionBo> resolveDeductionItems(XgDormHygieneCheckCompleteBo bo) {
if (bo.getDeductionItems() != null && !bo.getDeductionItems().isEmpty()) {
List<XgDormHygieneCheckDeductionBo> items = new ArrayList<>();
Set<Long> 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<XgDormHygieneCheckDeductionBo> 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);
}
} }
@@ -164,11 +164,7 @@ public class XgDormHygieneStudentScoreServiceImpl implements IXgDormHygieneStude
*/ */
@Override @Override
public List<XgDormHygieneStudentScoreVo> queryByHygieneCheckId(Long hygieneCheckId) { public List<XgDormHygieneStudentScoreVo> queryByHygieneCheckId(Long hygieneCheckId) {
LambdaQueryWrapper<XgDormHygieneStudentScore> lqw = Wrappers.lambdaQuery(); return baseMapper.selectVoListByHygieneCheckId(hygieneCheckId);
lqw.eq(XgDormHygieneStudentScore::getHygieneCheckId, hygieneCheckId);
lqw.eq(XgDormHygieneStudentScore::getDeleted, 0); // 只查询未删除的记录
lqw.orderByAsc(XgDormHygieneStudentScore::getId);
return baseMapper.selectVoList(lqw);
} }
/** /**
@@ -23,6 +23,8 @@ import org.dromara.xg.mapper.XgDormRollCallDetailMapper;
import org.dromara.xg.mapper.XgDormRoomMapper; import org.dromara.xg.mapper.XgDormRoomMapper;
import org.dromara.xg.mapper.XgDormBedMapper; import org.dromara.xg.mapper.XgDormBedMapper;
import org.dromara.xg.mapper.XgDormBuildingMapper; 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.XgDormRoom;
import org.dromara.xg.domain.XgDormBuilding; import org.dromara.xg.domain.XgDormBuilding;
import org.dromara.xg.domain.XgDormBed; import org.dromara.xg.domain.XgDormBed;
@@ -45,6 +47,7 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService {
private final XgDormRoomMapper roomMapper; private final XgDormRoomMapper roomMapper;
private final XgDormBedMapper bedMapper; private final XgDormBedMapper bedMapper;
private final XgDormBuildingMapper buildingMapper; private final XgDormBuildingMapper buildingMapper;
private final XgStudentMsgMapper studentMsgMapper;
@Override @Override
public XgDormRollCallVo queryById(Long id) { public XgDormRollCallVo queryById(Long id) {
@@ -274,6 +277,7 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService {
private void saveDetails(Long rollCallId, List<XgDormRollCallDetailBo> details) { private void saveDetails(Long rollCallId, List<XgDormRollCallDetailBo> details) {
if (details == null || details.isEmpty()) return; if (details == null || details.isEmpty()) return;
for (XgDormRollCallDetailBo detailBo : details) { for (XgDormRollCallDetailBo detailBo : details) {
fillDetailClassInfo(detailBo);
XgDormRollCallDetail detail = new XgDormRollCallDetail(); XgDormRollCallDetail detail = new XgDormRollCallDetail();
detail.setRollCallId(rollCallId); detail.setRollCallId(rollCallId);
detail.setBedId(detailBo.getBedId()); detail.setBedId(detailBo.getBedId());
@@ -296,4 +300,35 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService {
detailMapper.insert(detail); 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());
}
}
} }
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.xg.mapper.XgClassTeacherDormViewMapper">
<select id="selectClassByTeacherId" resultType="org.dromara.xg.domain.vo.ClassTeacherClassInfoVo">
SELECT
cm.id AS classId,
cm.class_name AS className,
cm.class_code AS classCode,
cm.college_name AS collegeName,
cm.major_name AS majorName
FROM xg_class_msg cm
WHERE cm.deleted = 0
AND cm.class_status = '1'
AND (
cm.class_teacher_id = #{teacherId}
OR EXISTS (
SELECT 1 FROM xg_class_teacher_counselor_rel rel
WHERE rel.class_id = cm.id
AND rel.teacher_id = #{teacherId}
AND rel.teacher_type = 'class_teacher'
AND rel.deleted = 0
AND (rel.end_date IS NULL OR rel.end_date &gt; NOW())
)
)
ORDER BY cm.create_time DESC
LIMIT 1
</select>
<select id="selectViolationPage" resultType="org.dromara.xg.domain.vo.ClassTeacherDormViolationVo">
SELECT * FROM (
SELECT
hcv.id AS id,
'hygiene_check' AS dataSource,
'卫生检查' AS dataSourceName,
hcv.student_id AS studentId,
hcv.student_no AS studentNo,
hcv.student_name AS studentName,
sm.class_name AS className,
hc.building_name AS buildingName,
hc.room_name AS roomName,
hcv.discipline_type_name AS violationTypeName,
dr.score_deduction AS scoreDeduction,
NULL AS violationLevel,
COALESCE(dr.discipline_date, DATE(hcv.create_time)) AS violationDate,
dr.discipline_reason COLLATE utf8mb4_general_ci AS violationReason,
hcv.status AS status,
hcv.create_time AS create_time
FROM xg_hygiene_check_violation hcv
INNER JOIN xskh_discipline_record dr ON hcv.discipline_record_id = dr.id AND dr.deleted = 0
INNER JOIN xg_dorm_hygiene_check hc ON hcv.hygiene_check_id = hc.id AND hc.deleted = 0
INNER JOIN xg_student_msg sm ON hcv.student_id = sm.id AND sm.deleted = 0
WHERE hcv.deleted = 0
AND sm.class_id = #{classId}
UNION ALL
SELECT
dv.id AS id,
'dorm_violation' AS dataSource,
'宿舍违纪登记' AS dataSourceName,
dv.student_id AS studentId,
dv.student_no AS studentNo,
dv.student_name AS studentName,
dv.class_name AS className,
dv.building_name AS buildingName,
dv.room_name AS roomName,
dv.violation_type AS violationTypeName,
NULL AS scoreDeduction,
dv.violation_level AS violationLevel,
COALESCE(dv.violation_time, dv.create_time) AS violationDate,
dv.violation_description AS violationReason,
'1' AS status,
dv.create_time AS create_time
FROM xg_dorm_violation dv
LEFT JOIN xg_student_msg sm2 ON dv.student_id = sm2.id AND sm2.deleted = 0
WHERE (dv.deleted = '0' OR dv.deleted = 0)
AND COALESCE(dv.class_id, sm2.class_id) = #{classId}
) t
WHERE 1 = 1
<if test="bo.beginDate != null and bo.beginDate != ''">
AND t.violationDate &gt;= CONCAT(#{bo.beginDate}, ' 00:00:00')
</if>
<if test="bo.endDate != null and bo.endDate != ''">
AND t.violationDate &lt;= CONCAT(#{bo.endDate}, ' 23:59:59')
</if>
<if test="bo.studentNo != null and bo.studentNo != ''">
AND t.studentNo LIKE CONCAT('%', #{bo.studentNo}, '%')
</if>
<if test="bo.studentName != null and bo.studentName != ''">
AND t.studentName LIKE CONCAT('%', #{bo.studentName}, '%')
</if>
ORDER BY t.create_time DESC
</select>
<select id="selectRollCallPage" resultType="org.dromara.xg.domain.vo.ClassTeacherDormRollCallVo">
SELECT
d.id AS id,
d.student_id AS studentId,
d.student_no AS studentNo,
d.student_name AS studentName,
d.class_name AS className,
r.batch_no AS batchNo,
r.check_date AS checkDate,
r.checker_name AS checkerName,
r.building_name AS buildingName,
r.room_name AS roomName,
d.bed_code AS bedCode,
d.roll_call_status AS rollCallStatus,
d.leave_type_name AS leaveTypeName,
r.remark AS remark
FROM xg_dorm_roll_call_detail d
INNER JOIN xg_dorm_roll_call r ON d.roll_call_id = r.id AND r.deleted = 0
INNER JOIN xg_student_msg sm ON d.student_id = sm.id AND sm.deleted = 0
WHERE d.deleted = 0
AND sm.class_id = #{classId}
AND d.roll_call_status != '3'
<if test="bo.beginDate != null and bo.beginDate != ''">
AND r.check_date &gt;= CONCAT(#{bo.beginDate}, ' 00:00:00')
</if>
<if test="bo.endDate != null and bo.endDate != ''">
AND r.check_date &lt;= CONCAT(#{bo.endDate}, ' 23:59:59')
</if>
<if test="bo.studentNo != null and bo.studentNo != ''">
AND d.student_no LIKE CONCAT('%', #{bo.studentNo}, '%')
</if>
<if test="bo.studentName != null and bo.studentName != ''">
AND d.student_name LIKE CONCAT('%', #{bo.studentName}, '%')
</if>
<if test="bo.rollCallStatus != null and bo.rollCallStatus != ''">
AND d.roll_call_status = #{bo.rollCallStatus}
</if>
ORDER BY r.check_date DESC, d.student_no ASC
</select>
</mapper>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.xg.mapper.XgDormHygieneCheckDeductionMapper">
<select id="selectVoListByHygieneCheckId" resultType="org.dromara.xg.domain.vo.XgDormHygieneCheckDeductionVo">
SELECT
d.id,
d.hygiene_check_id AS hygieneCheckId,
d.discipline_type_id AS disciplineTypeId,
d.discipline_type_name AS disciplineTypeName,
d.score,
d.sort_order AS sortOrder,
d.remark,
dt.min_score AS minScore,
dt.max_score AS maxScore
FROM xg_dorm_hygiene_check_deduction d
LEFT JOIN xskh_discipline_type dt ON dt.id = d.discipline_type_id AND dt.deleted = 0
WHERE d.hygiene_check_id = #{hygieneCheckId}
AND d.deleted = 0
ORDER BY d.sort_order ASC, d.id ASC
</select>
</mapper>
@@ -15,6 +15,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="buildingName" column="building_name" /> <result property="buildingName" column="building_name" />
<result property="roomName" column="room_name" /> <result property="roomName" column="room_name" />
<result property="totalScore" column="total_score" /> <result property="totalScore" column="total_score" />
<result property="disciplineTypeName" column="discipline_type_name" />
<result property="hygieneGradeText" column="hygiene_grade" /> <result property="hygieneGradeText" column="hygiene_grade" />
<result property="studentNo" column="student_no" /> <result property="studentNo" column="student_no" />
<result property="studentName" column="student_name" /> <result property="studentName" column="student_name" />
@@ -44,6 +45,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
dhc.building_name, dhc.building_name,
dhc.room_name, dhc.room_name,
dhc.total_score, dhc.total_score,
dhc.discipline_type_name,
dhc.hygiene_grade, dhc.hygiene_grade,
dhc.check_summary, dhc.check_summary,
dhc.improvement_suggestions, dhc.improvement_suggestions,
@@ -4,4 +4,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.xg.mapper.XgDormHygieneStudentScoreMapper"> <mapper namespace="org.dromara.xg.mapper.XgDormHygieneStudentScoreMapper">
<select id="selectVoListByHygieneCheckId" resultType="org.dromara.xg.domain.vo.XgDormHygieneStudentScoreVo">
SELECT
ss.id,
ss.deleted,
ss.hygiene_check_id AS hygieneCheckId,
ss.student_id AS studentId,
ss.student_no AS studentNo,
ss.student_name AS studentName,
ss.class_id AS classId,
ss.class_name AS className,
ss.major_id AS majorId,
ss.major_name AS majorName,
ss.college_id AS collegeId,
ss.college_name AS collegeName,
ss.bed_id AS bedId,
ss.bed_code AS bedCode,
ss.student_score AS studentScore,
ss.attendance_status AS attendanceStatus,
ss.student_grade AS studentGrade,
ss.is_responsible_person AS isResponsiblePerson,
ss.score_remarks AS scoreRemarks,
ss.improvement_notes AS improvementNotes,
COALESCE(sm_by_id.gender, sm_by_no.gender) AS gender
FROM xg_dorm_hygiene_student_score ss
LEFT JOIN xg_student_msg sm_by_id ON sm_by_id.id = ss.student_id AND sm_by_id.deleted = 0
LEFT JOIN xg_student_msg sm_by_no ON sm_by_no.student_no = ss.student_no AND sm_by_no.deleted = 0
WHERE ss.hygiene_check_id = #{hygieneCheckId}
AND ss.deleted = 0
ORDER BY ss.id ASC
</select>
</mapper> </mapper>
+1
View File
@@ -180,6 +180,7 @@
| [班级干部信息管理API](./班级干部信息管理API.md) | 班级干部信息管理(含用户角色关联) | ✅ 完成 | 2025-01-27 | | [班级干部信息管理API](./班级干部信息管理API.md) | 班级干部信息管理(含用户角色关联) | ✅ 完成 | 2025-01-27 |
| [班干部管理API](./班干部管理API.md) | 班级干部职务管理 | ✅ 完成 | 2025-01-27 | | [班干部管理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](./学生考核管理系统API.md)** | **学生综合素质测评、违纪处分、奖励加分管理** | **✅ 完成** | **2025-01-27** |
| **[学生考核管理系统修改方案](./学生考核管理系统API修改方案.md)** | **基于标准化违纪处分等级的系统升级方案** | **📋 方案** | **2025-01-27** | | **[学生考核管理系统修改方案](./学生考核管理系统API修改方案.md)** | **基于标准化违纪处分等级的系统升级方案** | **📋 方案** | **2025-01-27** |
| **[学生考核管理系统数据库修改影响分析](./学生考核管理系统数据库修改影响分析.md)** | **数据库升级脚本执行的详细影响分析** | **📊 分析** | **2025-01-27** | | **[学生考核管理系统数据库修改影响分析](./学生考核管理系统数据库修改影响分析.md)** | **数据库升级脚本执行的详细影响分析** | **📊 分析** | **2025-01-27** |
@@ -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接口规范 ## API接口规范
### 1. 新增宿舍卫生检查(包含学生评分) ### 1. 新增宿舍卫生检查(包含学生评分)
@@ -39,6 +60,20 @@
"campusId": 1, "campusId": 1,
"campusName": "主校区", "campusName": "主校区",
"totalScore": 85.5, "totalScore": 85.5,
"deductionItems": [
{
"disciplineTypeId": 1963793291531026434,
"disciplineTypeName": "地面未打扫",
"score": 5,
"sortOrder": 1
},
{
"disciplineTypeId": 1963793291531026435,
"disciplineTypeName": "物品摆放凌乱",
"score": 3,
"sortOrder": 2
}
],
"hygieneGrade": "B", "hygieneGrade": "B",
"attachmentIds": "oss1,oss2,oss3", "attachmentIds": "oss1,oss2,oss3",
"checkSummary": "宿舍整体卫生良好", "checkSummary": "宿舍整体卫生良好",
@@ -280,7 +315,8 @@ public interface DormHygieneStudentScoreMapper extends BaseMapperPlus<DormHygien
- `checkDate`: 必填,日期时间格式 - `checkDate`: 必填,日期时间格式
- `checkerId`: 必填,关联用户表 - `checkerId`: 必填,关联用户表
- `roomId`: 必填,关联房间表 - `roomId`: 必填,关联房间表
- `totalScore`: 可选,0-100分 - `totalScore`: 可选,0-100分(由 deductionItems 合计自动计算,只读)
- `deductionItems`: 可选,扣分明细数组;每项 score 默认 minScore,可在类型 min-max 内调整;合计不超过100
- `hygieneGrade`: 可选,A/B/C/D - `hygieneGrade`: 可选,A/B/C/D
### 学生评分表验证 ### 学生评分表验证
@@ -11,15 +11,15 @@
</view> </view>
<u-form ref="formRef" :model="formData" :rules="rules" label-width="100"> <u-form ref="formRef" :model="formData" :rules="rules" label-width="100">
<!-- 违纪类型 --> <!-- 扣分项(多选) -->
<u-form-item label="违纪类型" prop="disciplineTypeId"> <u-form-item label="扣分项">
<view <view
@click="!isViewMode && (showDisciplineTypePicker = true)" @click="!isViewMode && openDeductionPopup()"
style="width: 100%" style="width: 100%"
> >
<u-input <u-input
:value="disciplineTypeDisplay" :value="deductionSummaryDisplay"
placeholder="请选择违纪类型" placeholder="请选择扣分项(可多选)"
readonly readonly
border="none" border="none"
:suffix-icon="isViewMode ? '' : 'arrow-down'" :suffix-icon="isViewMode ? '' : 'arrow-down'"
@@ -27,38 +27,70 @@
style="pointer-events: none" style="pointer-events: none"
/> />
</view> </view>
<u-picker
v-if="!isViewMode"
:show="showDisciplineTypePicker && disciplineTypeOptions.length > 0"
:columns="disciplineTypeColumns"
keyName="text"
@confirm="handleDisciplineTypeConfirm"
@cancel="showDisciplineTypePicker = false"
></u-picker>
</u-form-item> </u-form-item>
<!-- 违纪类型说明 --> <view v-if="deductionItems.length > 0" class="deduction-list">
<view <view
v-if="formData.disciplineTypeId && selectedDisciplineTypeRemark" v-for="(item, index) in deductionItems"
class="discipline-type-remark" :key="item.disciplineTypeId"
> class="deduction-item"
<u-icon name="info-circle" color="#909399" size="14"></u-icon> >
<text class="remark-text"> <view class="deduction-item-header">
【{{ formData.disciplineTypeName }}】类型说明:{{ <text class="deduction-name">{{ item.disciplineTypeName }}</text>
selectedDisciplineTypeRemark <u-input
}} v-if="!isViewMode"
</text> v-model="item.score"
type="digit"
placeholder="扣分"
@input="handleDeductionScoreChange"
/>
<text v-else class="deduction-score">{{ item.score }}分</text>
<text class="deduction-range">({{ getDeductionMin(item) }}-{{ getDeductionMax(item) }}分)</text>
<u-icon
v-if="!isViewMode"
name="close-circle"
color="#f56c6c"
size="18"
@click="removeDeductionItem(index)"
></u-icon>
</view>
<view v-if="getDeductionRemark(item.disciplineTypeId)" class="deduction-remark">
<text>{{ getDeductionRemark(item.disciplineTypeId) }}</text>
</view>
</view>
<view class="deduction-total">
合计扣分:<text class="total-value">{{ formData.totalScore }}</text> 分
</view>
</view> </view>
<u-popup :show="showDeductionPopup" mode="bottom" round="16" @close="showDeductionPopup = false">
<view class="deduction-popup">
<view class="deduction-popup-header">
<text class="deduction-popup-title">选择扣分项</text>
<text class="deduction-popup-action" @click="confirmDeductionSelection">确定</text>
</view>
<scroll-view scroll-y class="deduction-popup-body">
<u-checkbox-group v-model="tempSelectedTypeIds" placement="column">
<u-checkbox
v-for="item in disciplineTypeOptions"
:key="item.id"
:name="String(item.id)"
:label="`${item.typeName}(${item.minScore || 0}分起)`"
customStyle="margin-bottom: 12px;"
></u-checkbox>
</u-checkbox-group>
</scroll-view>
</view>
</u-popup>
<!-- 宿舍扣分 --> <!-- 宿舍扣分 -->
<u-form-item label="宿舍扣分" prop="totalScore"> <u-form-item label="宿舍扣分" prop="totalScore">
<u-input <u-input
v-model.number="formData.totalScore" v-model="formData.totalScore"
type="digit" type="digit"
placeholder="请输入扣分(0-100分)" placeholder="自动合计"
:disabled="isViewMode" disabled
:readonly="isViewMode" readonly
@input="handleTotalScoreChange"
/> />
</u-form-item> </u-form-item>
@@ -272,25 +304,9 @@
<text class="student-name">{{ student.studentName }}</text> <text class="student-name">{{ student.studentName }}</text>
<!-- 性别标签 --> <!-- 性别标签 -->
<u-tag <u-tag
v-if=" v-if="normalizeGender(student.gender)"
student.gender !== undefined && :text="normalizeGender(student.gender) === '0' ? '男' : '女'"
student.gender !== null && :type="normalizeGender(student.gender) === '0' ? 'primary' : 'error'"
student.gender !== ''
"
:text="
student.gender === '0'
? '男'
: student.gender === '1'
? '女'
: '未知'
"
:type="
student.gender === '0'
? 'primary'
: student.gender === '1'
? 'error'
: 'warning'
"
size="mini" size="mini"
></u-tag> ></u-tag>
</view> </view>
@@ -583,6 +599,8 @@ import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import weChatSDK from '@/utils/wechat.js'; import weChatSDK from '@/utils/wechat.js';
import ViolationRecordDialog from './ViolationRecordDialog.vue'; import ViolationRecordDialog from './ViolationRecordDialog.vue';
import { normalizeStudentId, normalizeId } from '../utils/studentId.js';
import { calcStudentViolationScoreTotal } from '../utils/violationScore.js';
export default { export default {
name: 'SingleRoomCheck', name: 'SingleRoomCheck',
@@ -622,13 +640,16 @@ export default {
attachmentIds: '', attachmentIds: '',
}, },
deductionItems: [],
showDeductionPopup: false,
tempSelectedTypeIds: [],
// 表单验证规则 // 表单验证规则
rules: { rules: {
hygieneGrade: [{ required: true, message: '请选择卫生等级', trigger: 'change' }], hygieneGrade: [{ required: true, message: '请选择卫生等级', trigger: 'change' }],
}, },
// Picker显示状态 // Picker显示状态
showDisciplineTypePicker: false,
showHygieneGradePicker: false, showHygieneGradePicker: false,
// 违纪类型选项 // 违纪类型选项
@@ -692,32 +713,9 @@ export default {
return name; return name;
}, },
// 违纪类型列 deductionSummaryDisplay() {
disciplineTypeColumns() { if (!this.deductionItems.length) return '';
return [ return this.deductionItems.map((item) => item.disciplineTypeName).join('、');
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 : '';
}, },
// 卫生等级列 // 卫生等级列
@@ -822,12 +820,10 @@ export default {
// 加载违纪类型 // 加载违纪类型
await this.loadDisciplineTypes(); await this.loadDisciplineTypes();
// 加载房间学生信息 if (this.existingData?.id) {
await this.loadRoomStudents(); await this.loadEditData();
} else {
// 如果有现有数据,加载编辑数据 await this.loadRoomStudents();
if (this.existingData) {
this.loadEditData();
} }
} catch (error) { } catch (error) {
console.error('初始化组件失败:', 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() { async loadDisciplineTypes() {
try { try {
@@ -897,6 +939,7 @@ export default {
this.roomStudents = res.data.map((student) => ({ this.roomStudents = res.data.map((student) => ({
...student, ...student,
studentId: normalizeStudentId(student.studentId),
student_score: 0, student_score: 0,
attendance_status: '1', attendance_status: '1',
student_grade: 'A', student_grade: 'A',
@@ -937,10 +980,10 @@ export default {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
this.studentLeaves.clear(); this.studentLeaves.clear();
Object.keys(res.data).forEach((studentId) => { 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( const student = this.roomStudents.find(
(s) => s.studentId === Number(studentId), (s) => normalizeStudentId(s.studentId) === normalizedId,
); );
if (student) { if (student) {
student.attendance_status = '3'; student.attendance_status = '3';
@@ -969,6 +1012,7 @@ export default {
this.formData.hygieneGrade = this.existingData.hygieneGrade || 'A'; this.formData.hygieneGrade = this.existingData.hygieneGrade || 'A';
this.formData.checkRemarks = this.existingData.checkRemarks || ''; this.formData.checkRemarks = this.existingData.checkRemarks || '';
this.formData.attachmentIds = this.existingData.attachmentIds || ''; this.formData.attachmentIds = this.existingData.attachmentIds || '';
this.initDeductionItemsFromData(this.existingData);
console.log('✅ 表单基础数据回显完成:', this.formData); console.log('✅ 表单基础数据回显完成:', this.formData);
@@ -980,7 +1024,7 @@ export default {
// 3️⃣ 从后端接口加载学生评分数据、违纪记录和请假信息(复用Web端接口) // 3️⃣ 从后端接口加载学生评分数据、违纪记录和请假信息(复用Web端接口)
// 注意:loadStudentScoresFromAPI 内部会自动调用 loadViolationRecordsFromAPI 和 loadStudentLeaves // 注意:loadStudentScoresFromAPI 内部会自动调用 loadViolationRecordsFromAPI 和 loadStudentLeaves
if (this.existingData.id) { if (this.existingData.id) {
await this.loadStudentScoresFromAPI(this.existingData.id); await this.loadStudentScoresFromAPI(normalizeId(this.existingData.id));
} }
console.log('✅ 所有数据回显完成'); console.log('✅ 所有数据回显完成');
@@ -992,17 +1036,18 @@ export default {
// 从后端API加载学生评分数据(复用Web端接口) // 从后端API加载学生评分数据(复用Web端接口)
async loadStudentScoresFromAPI(checkId) { async loadStudentScoresFromAPI(checkId) {
if (!checkId) { const normalizedCheckId = normalizeId(checkId);
if (!normalizedCheckId) {
console.warn('⚠️ 没有检查ID,无法加载学生评分数据'); console.warn('⚠️ 没有检查ID,无法加载学生评分数据');
return; return;
} }
this.studentsLoading = true; this.studentsLoading = true;
try { try {
console.log('🔄 从API加载学生评分数据,检查ID:', checkId); console.log('🔄 从API加载学生评分数据,检查ID:', normalizedCheckId);
const res = await request({ const res = await request({
url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${checkId}`, url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${normalizedCheckId}`,
method: 'GET', method: 'GET',
}); });
@@ -1010,9 +1055,9 @@ export default {
console.log('✅ 成功获取学生评分数据:', res.data.length, '条'); console.log('✅ 成功获取学生评分数据:', res.data.length, '条');
// 将后端返回的学生评分数据映射到roomStudents // 将后端返回的学生评分数据映射到roomStudents
this.roomStudents = res.data.map((studentScore) => ({ let students = res.data.map((studentScore) => ({
// 学生基本信息 // 学生基本信息
studentId: studentScore.studentId, studentId: normalizeStudentId(studentScore.studentId),
studentName: studentScore.studentName, studentName: studentScore.studentName,
studentNo: studentScore.studentNo, studentNo: studentScore.studentNo,
className: studentScore.className, className: studentScore.className,
@@ -1021,7 +1066,7 @@ export default {
bedId: studentScore.bedId, bedId: studentScore.bedId,
bedName: studentScore.bedCode, bedName: studentScore.bedCode,
bedCode: studentScore.bedCode, bedCode: studentScore.bedCode,
gender: studentScore.gender || '1', gender: this.normalizeGender(studentScore.gender),
// 评分数据 // 评分数据
student_score: studentScore.studentScore || 0, student_score: studentScore.studentScore || 0,
attendance_status: studentScore.attendanceStatus || '1', attendance_status: studentScore.attendanceStatus || '1',
@@ -1034,13 +1079,27 @@ export default {
showGradePicker: false, 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('✅ 学生评分数据映射完成'); console.log('✅ 学生评分数据映射完成');
// 🔴 同时加载违纪记录和请假信息(复用Web端逻辑) // 🔴 同时加载违纪记录和请假信息(复用Web端逻辑)
await Promise.all([ await Promise.all([
this.loadViolationRecordsFromAPI(checkId), this.loadViolationRecordsFromAPI(normalizedCheckId),
this.loadStudentLeaves(), this.loadStudentLeaves(),
]); ]);
this.syncStudentScoreFromViolations();
} else { } else {
console.warn('⚠️ 未获取到学生评分数据,保持当前学生列表'); console.warn('⚠️ 未获取到学生评分数据,保持当前学生列表');
} }
@@ -1054,16 +1113,17 @@ export default {
// 从后端API加载违纪记录数据(复用Web端接口) // 从后端API加载违纪记录数据(复用Web端接口)
async loadViolationRecordsFromAPI(checkId) { async loadViolationRecordsFromAPI(checkId) {
if (!checkId) { const normalizedCheckId = normalizeId(checkId);
if (!normalizedCheckId) {
console.warn('⚠️ 没有检查ID,无法加载违纪记录数据'); console.warn('⚠️ 没有检查ID,无法加载违纪记录数据');
return; return;
} }
try { try {
console.log('🔄 从API加载违纪记录数据,检查ID:', checkId); console.log('🔄 从API加载违纪记录数据,检查ID:', normalizedCheckId);
const res = await request({ const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${checkId}`, url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${normalizedCheckId}`,
method: 'GET', method: 'GET',
}); });
@@ -1075,7 +1135,7 @@ export default {
// 按学生ID分组违纪记录 // 按学生ID分组违纪记录
res.data.forEach((violation) => { res.data.forEach((violation) => {
const studentId = violation.studentId; const studentId = normalizeStudentId(violation.studentId);
if (!studentId) return; if (!studentId) return;
const violations = this.violationRecords.get(studentId) || []; const violations = this.violationRecords.get(studentId) || [];
@@ -1101,6 +1161,7 @@ export default {
'✅ 所有违纪记录数据加载完成,总学生数:', '✅ 所有违纪记录数据加载完成,总学生数:',
this.violationRecords.size, this.violationRecords.size,
); );
this.syncStudentScoreFromViolations();
} else { } else {
console.warn('⚠️ 未获取到违纪记录数据'); console.warn('⚠️ 未获取到违纪记录数据');
} }
@@ -1318,7 +1379,9 @@ export default {
console.log('👥 回显学生评分数据:', studentScores); console.log('👥 回显学生评分数据:', studentScores);
studentScores.forEach((scoreData) => { 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) { if (student) {
student.student_score = scoreData.studentScore || 0; student.student_score = scoreData.studentScore || 0;
student.attendance_status = scoreData.attendanceStatus || '1'; student.attendance_status = scoreData.attendanceStatus || '1';
@@ -1337,7 +1400,7 @@ export default {
this.violationRecords.clear(); this.violationRecords.clear();
violationData.forEach((violation) => { violationData.forEach((violation) => {
const studentId = violation.studentId; const studentId = normalizeStudentId(violation.studentId);
if (!studentId) return; if (!studentId) return;
const violations = this.violationRecords.get(studentId) || []; const violations = this.violationRecords.get(studentId) || [];
@@ -1360,6 +1423,28 @@ export default {
violation.violationAttachmentIds || violation.attachmentOssIds, 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,防止内存泄漏 // 清理blob URL,防止内存泄漏
@@ -1377,38 +1462,104 @@ export default {
}); });
}, },
// 违纪类型确认处理 initDeductionItemsFromData(data) {
handleDisciplineTypeConfirm(e) { if (data?.deductionItems && data.deductionItems.length > 0) {
if (e && e.value && e.value.length > 0) { this.deductionItems = data.deductionItems.map((item, index) => ({
const selectedType = this.disciplineTypeOptions.find( ...item,
(item) => item.id === e.value[0].value, score: item.score ?? 0,
); sortOrder: item.sortOrder ?? index + 1,
if (selectedType) { }));
this.formData.disciplineTypeId = selectedType.id; } else if (data?.disciplineTypeId) {
this.formData.disciplineTypeName = selectedType.typeName; this.deductionItems = [
{
// 自动填入最低扣分 disciplineTypeId: data.disciplineTypeId,
if (selectedType.minScore !== null && selectedType.minScore !== undefined) { disciplineTypeName: data.disciplineTypeName,
this.formData.totalScore = selectedType.minScore; score: data.totalScore || 0,
uni.showToast({ sortOrder: 1,
title: `已自动填入最低扣分:${selectedType.minScore}分`, },
icon: 'none', ];
}); } else {
} this.deductionItems = [];
}
} }
this.showDisciplineTypePicker = false; this.recalcDeductionTotal();
}, },
// 宿舍总分变化处理 openDeductionPopup() {
handleTotalScoreChange() { this.tempSelectedTypeIds = this.deductionItems.map((item) => String(item.disciplineTypeId));
const score = parseFloat(this.formData.totalScore); this.showDeductionPopup = true;
if (!isNaN(score) && (score < 0 || score > 100)) { },
uni.showToast({ title: '宿舍扣分必须在0-100分之间', icon: 'none' });
this.formData.totalScore = 0; 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) { handleHygieneGradeConfirm(e) {
if (e && e.value && e.value.length > 0) { if (e && e.value && e.value.length > 0) {
@@ -2114,7 +2265,7 @@ export default {
// 获取学生请假信息 // 获取学生请假信息
getStudentLeave(studentId) { getStudentLeave(studentId) {
return this.studentLeaves.get(studentId) || null; return this.studentLeaves.get(normalizeStudentId(studentId)) || null;
}, },
// 格式化日期时间 // 格式化日期时间
@@ -2141,8 +2292,7 @@ export default {
// 删除违纪记录 // 删除违纪记录
removeViolationRecord(studentId, index) { removeViolationRecord(studentId, index) {
// 保存要删除的记录信息 this.deleteViolationStudentId = normalizeStudentId(studentId);
this.deleteViolationStudentId = studentId;
this.deleteViolationIndex = index; this.deleteViolationIndex = index;
// 显示确认对话框 // 显示确认对话框
this.showDeleteConfirm = true; this.showDeleteConfirm = true;
@@ -2159,6 +2309,7 @@ export default {
} else { } else {
this.violationRecords.set(this.deleteViolationStudentId, violations); this.violationRecords.set(this.deleteViolationStudentId, violations);
} }
this.syncStudentScoreFromViolations(this.deleteViolationStudentId);
uni.showToast({ title: '违纪记录删除成功', icon: 'success' }); uni.showToast({ title: '违纪记录删除成功', icon: 'success' });
} }
@@ -2178,25 +2329,31 @@ export default {
// 获取学生违纪记录 // 获取学生违纪记录
getStudentViolations(studentId) { getStudentViolations(studentId) {
return this.violationRecords.get(studentId) || []; return this.violationRecords.get(normalizeStudentId(studentId)) || [];
}, },
// 违纪记录确认 // 违纪记录确认
handleViolationConfirm(violationData) { handleViolationConfirm(violationData) {
const studentId = this.currentStudent.studentId; const studentId = normalizeStudentId(this.currentStudent.studentId);
const violations = this.violationRecords.get(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; violations[this.currentViolationIndex] = violationData;
uni.showToast({ title: '违纪记录保存成功', icon: 'success' });
} else { } else {
// 新增模式
violations.push(violationData); violations.push(violationData);
uni.showToast({ title: '违纪记录保存成功', icon: 'success' });
} }
this.violationRecords.set(studentId, violations); this.violationRecords.set(studentId, violations);
this.syncStudentScoreFromViolations(studentId);
this.violationDialogVisible = false; this.violationDialogVisible = false;
uni.showToast({ title: '违纪记录保存成功', icon: 'success' });
}, },
// 违纪记录弹窗关闭 // 违纪记录弹窗关闭
@@ -2210,8 +2367,15 @@ export default {
// 表单验证 // 表单验证
await this.$refs.formRef.validate(); await this.$refs.formRef.validate();
if ((this.formData.totalScore || 0) > 100) {
uni.showToast({ title: '宿舍合计扣分不能超过100分', icon: 'none' });
return;
}
this.submitLoading = true; this.submitLoading = true;
this.syncStudentScoreFromViolations();
// 处理附件数据 // 处理附件数据
const attachmentOssIds = this.attachmentList const attachmentOssIds = this.attachmentList
.filter((file) => file.status === 'success' && file.ossId) .filter((file) => file.status === 'success' && file.ossId)
@@ -2270,6 +2434,7 @@ export default {
// 宿舍整体评分 // 宿舍整体评分
...this.formData, ...this.formData,
deductionItems: this.deductionItems,
// 学生评分数据 // 学生评分数据
studentScores, studentScores,
@@ -2791,4 +2956,90 @@ export default {
padding: 20rpx 30rpx; padding: 20rpx 30rpx;
padding-bottom: 40rpx; 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;
}
</style> </style>
@@ -36,40 +36,120 @@
</view> </view>
</view> </view>
<!-- 违纪类型选择 --> <!-- 违纪类型:编辑模式单选 -->
<u-form-item label="违纪类型" prop="disciplineTypeId" required> <template v-if="isEditMode">
<view @click="handleDisciplineTypeClick" style="width: 100%"> <u-form-item label="违纪类型" prop="disciplineTypeId" required>
<u-input <view @click="handleDisciplineTypeClick" style="width: 100%">
:value="disciplineTypeDisplay" <u-input
placeholder="请选择违纪类型" :value="disciplineTypeDisplay"
readonly placeholder="请选择违纪类型"
suffix-icon="arrow-down" readonly
suffix-icon-style="color: #c8c9cc;" suffix-icon="arrow-down"
@click.stop="handleDisciplineTypeClick" suffix-icon-style="color: #c8c9cc;"
/> @click.stop="handleDisciplineTypeClick"
/>
</view>
</u-form-item>
<u-picker
:show="showDisciplineTypePicker"
:columns="disciplineTypeColumns"
keyName="text"
@confirm="handleDisciplineTypeConfirm"
@cancel="showDisciplineTypePicker = false"
@close="showDisciplineTypePicker = false"
></u-picker>
<view
v-if="formData.disciplineTypeId && selectedDisciplineTypeRemark"
class="discipline-type-remark"
>
<u-icon name="info-circle" color="#909399" size="14"></u-icon>
<text class="remark-text">
类型说明:{{ selectedDisciplineTypeRemark }}
</text>
</view> </view>
</u-form-item> </template>
<!-- 违纪类型选择器 --> <!-- 违纪类型:新增模式多选 -->
<u-picker <template v-else>
:show="showDisciplineTypePicker" <u-form-item label="违纪类型">
:columns="disciplineTypeColumns" <view @click="openViolationTypePopup" style="width: 100%">
keyName="text" <u-input
@confirm="handleDisciplineTypeConfirm" :value="violationSummaryDisplay"
@cancel="showDisciplineTypePicker = false" placeholder="请选择违纪类型(可多选)"
@close="showDisciplineTypePicker = false" readonly
></u-picker> suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
</u-form-item>
<!-- 违纪类型说明 --> <view v-if="violationItems.length > 0" class="violation-item-list">
<view <view
v-if="formData.disciplineTypeId && selectedDisciplineTypeRemark" v-for="(item, index) in violationItems"
class="discipline-type-remark" :key="item.disciplineTypeId"
> class="violation-item-row"
<u-icon name="info-circle" color="#909399" size="14"></u-icon> >
<text class="remark-text"> <view class="violation-item-header">
类型说明:{{ selectedDisciplineTypeRemark }} <text class="violation-item-name">{{ item.disciplineTypeName }}</text>
</text> <u-input
</view> v-model="item.scoreDeduction"
type="digit"
placeholder="扣分"
@input="handleViolationScoreChange"
/>
<text class="score-range">
({{ getViolationMin(item) }}-{{ getViolationMax(item) }}分)
</text>
<u-icon
name="close-circle"
color="#f56c6c"
size="18"
@click="removeViolationItem(index)"
></u-icon>
</view>
<view
v-if="getViolationRemark(item.disciplineTypeId)"
class="discipline-type-remark"
>
<text>类型说明:{{ getViolationRemark(item.disciplineTypeId) }}</text>
</view>
</view>
<view class="violation-item-total">
合计扣分:<text class="total-value">{{ violationItemsTotalScore }}</text> 分
</view>
</view>
<u-popup
:show="showViolationTypePopup"
mode="bottom"
round="16"
@close="showViolationTypePopup = false"
>
<view class="violation-type-popup">
<view class="violation-type-popup-header">
<text class="violation-type-popup-title">选择违纪类型</text>
<text
class="violation-type-popup-action"
@click="confirmViolationTypeSelection"
>确定</text>
</view>
<scroll-view scroll-y class="violation-type-popup-body">
<u-checkbox-group v-model="tempSelectedTypeIds" placement="column">
<u-checkbox
v-for="item in allViolationTypes"
:key="item.id"
:name="String(item.id)"
:label="getViolationTypeLabel(item)"
customStyle="margin-bottom: 12px;"
></u-checkbox>
</u-checkbox-group>
</scroll-view>
</view>
</u-popup>
</template>
<!-- 违纪日期 --> <!-- 违纪日期 -->
<u-form-item label="违纪日期" prop="disciplineDate" required> <u-form-item label="违纪日期" prop="disciplineDate" required>
@@ -93,8 +173,13 @@
></u-datetime-picker> ></u-datetime-picker>
</u-form-item> </u-form-item>
<!-- 扣分分值 --> <!-- 扣分分值(编辑模式) -->
<u-form-item label="扣分分值" prop="scoreDeduction" required> <u-form-item
v-if="isEditMode"
label="扣分分值"
prop="scoreDeduction"
required
>
<u-input <u-input
v-model.number="formData.scoreDeduction" v-model.number="formData.scoreDeduction"
type="digit" type="digit"
@@ -104,7 +189,7 @@
</u-form-item> </u-form-item>
<!-- 违纪原因 --> <!-- 违纪原因 -->
<u-form-item label="违纪原因" prop="disciplineReason" required> <u-form-item label="违纪原因" prop="disciplineReason">
<u-textarea <u-textarea
v-model="formData.disciplineReason" v-model="formData.disciplineReason"
placeholder="请输入违纪原因" placeholder="请输入违纪原因"
@@ -398,11 +483,14 @@ export default {
}, },
}, },
], ],
disciplineReason: [{ required: true, message: '请输入违纪原因', trigger: 'blur' }], disciplineReason: [],
}, },
// Picker显示状态 // Picker显示状态
showDisciplineTypePicker: false, showDisciplineTypePicker: false,
showViolationTypePopup: false,
tempSelectedTypeIds: [],
violationItems: [],
showDatePicker: false, showDatePicker: false,
// 违纪日期 // 违纪日期
@@ -435,6 +523,23 @@ export default {
}; };
}, },
computed: { computed: {
isEditMode() {
return !!this.violationData;
},
violationSummaryDisplay() {
if (!this.violationItems.length) return '';
return this.violationItems.map((item) => item.disciplineTypeName).join('、');
},
violationItemsTotalScore() {
const sum = this.violationItems.reduce(
(acc, item) => acc + (parseFloat(item.scoreDeduction) || 0),
0,
);
return Number(sum.toFixed(2));
},
// 违纪类型列(带区域标签) // 违纪类型列(带区域标签)
disciplineTypeColumns() { disciplineTypeColumns() {
return [ return [
@@ -768,8 +873,9 @@ export default {
} }
} else { } else {
console.log('违纪记录:新增模式,重置附件列表'); console.log('违纪记录:新增模式,重置附件列表');
// 重置附件列表
this.attachmentList = []; this.attachmentList = [];
this.violationItems = [];
this.tempSelectedTypeIds = [];
} }
}, },
@@ -919,6 +1025,11 @@ export default {
// 违纪类型点击处理 // 违纪类型点击处理
handleDisciplineTypeClick() { handleDisciplineTypeClick() {
if (!this.isEditMode) {
this.openViolationTypePopup();
return;
}
console.log('=== 违纪类型点击 ==='); console.log('=== 违纪类型点击 ===');
console.log('违纪类型数据量:', this.allViolationTypes.length); console.log('违纪类型数据量:', this.allViolationTypes.length);
console.log('违纪类型数据:', this.allViolationTypes); console.log('违纪类型数据:', this.allViolationTypes);
@@ -996,6 +1107,100 @@ export default {
this.showDisciplineTypePicker = false; 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() { handleScoreChange() {
// 确保转换为数字类型 // 确保转换为数字类型
@@ -1844,18 +2049,55 @@ export default {
// 确定 // 确定
async handleConfirm() { async handleConfirm() {
try { try {
// 表单验证
await this.$refs.formRef.validate();
this.confirmLoading = true; this.confirmLoading = true;
// 处理附件数据 if (this.isEditMode) {
const attachmentOssIds = this.attachmentList await this.$refs.formRef.validate();
.filter((file) => file.status === 'success' && file.ossId) const attachmentOssIds = this.attachmentList
.map((file) => file.ossId); .filter((file) => file.status === 'success' && file.ossId)
this.formData.attachmentOssIds = attachmentOssIds.join(','); .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) { } catch (error) {
console.error('提交失败:', error); console.error('提交失败:', error);
uni.showToast({ title: '请检查表单填写是否完整', icon: 'none' }); uni.showToast({ title: '请检查表单填写是否完整', icon: 'none' });
@@ -1915,6 +2157,9 @@ export default {
this.currentTypeMaxScore = 100; this.currentTypeMaxScore = 100;
this.disciplineDateDisplay = dayjs().format('YYYY-MM-DD'); this.disciplineDateDisplay = dayjs().format('YYYY-MM-DD');
this.disciplineDateValue = Number(new Date()); this.disciplineDateValue = Number(new Date());
this.violationItems = [];
this.tempSelectedTypeIds = [];
this.showViolationTypePopup = false;
}, },
}, },
}; };
@@ -2200,4 +2445,76 @@ export default {
padding-bottom: 40rpx; padding-bottom: 40rpx;
border-top: 2rpx solid #f0f0f0; 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;
}
</style> </style>
@@ -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;
@@ -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));
}
+12 -5
View File
@@ -270,6 +270,7 @@
import { request } from '@/common/request.js'; import { request } from '@/common/request.js';
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js'; import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import SingleRoomCheck from './components/SingleRoomCheck.vue'; import SingleRoomCheck from './components/SingleRoomCheck.vue';
import { normalizeId } from './utils/studentId.js';
export default { export default {
name: 'DormHygieneCheck', name: 'DormHygieneCheck',
@@ -544,7 +545,7 @@ export default {
// 加载检查记录详情 // 加载检查记录详情
const res = await request({ const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`, url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`,
method: 'GET', method: 'GET',
}); });
@@ -552,7 +553,10 @@ export default {
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
this.currentViewItem = item; this.currentViewItem = item;
this.currentViewData = res.data; this.currentViewData = {
...res.data,
id: normalizeId(res.data.id),
};
// 构造房间信息 // 构造房间信息
this.currentViewRoomInfo = { this.currentViewRoomInfo = {
@@ -605,7 +609,7 @@ export default {
// 加载检查记录详情 // 加载检查记录详情
const res = await request({ const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`, url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`,
method: 'GET', method: 'GET',
}); });
@@ -613,7 +617,10 @@ export default {
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
this.currentEditItem = item; this.currentEditItem = item;
this.currentEditData = res.data; this.currentEditData = {
...res.data,
id: normalizeId(res.data.id),
};
// 构造房间信息 // 构造房间信息
this.currentRoomInfo = { this.currentRoomInfo = {
@@ -708,7 +715,7 @@ export default {
uni.showLoading({ title: '删除中...' }); uni.showLoading({ title: '删除中...' });
const res = await request({ const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`, url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`,
method: 'DELETE', method: 'DELETE',
}); });
+61 -1
View File
@@ -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() { async loadRoomStudents() {
if (!this.formData.roomId) { if (!this.formData.roomId) {
@@ -1329,8 +1375,9 @@ export default {
studentScoresRes.data && studentScoresRes.data &&
studentScoresRes.data.length > 0 studentScoresRes.data.length > 0
) { ) {
this.roomStudents = studentScoresRes.data.map((item) => ({ let students = studentScoresRes.data.map((item) => ({
...item, ...item,
gender: this.normalizeGender(item.gender),
student_score: item.studentScore || null, student_score: item.studentScore || null,
attendance_status: item.attendanceStatus || '1', attendance_status: item.attendanceStatus || '1',
student_grade: item.studentGrade || 'A', student_grade: item.studentGrade || 'A',
@@ -1339,6 +1386,19 @@ export default {
bedCode: item.bedCode, bedCode: item.bedCode,
bedName: 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; return;
} }
} }
@@ -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<T = any>(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<ClassTeacherClassInfo> => {
return request({
url: '/classTeacherDorm/classTeacherDorm/myClass',
method: 'get'
}).then((res) => unwrapResponse(res, '获取班级信息'));
};
/** 本班宿舍违纪列表 */
export const listClassTeacherViolations = (
query?: ClassTeacherDormQuery
): AxiosPromise<TableDataInfo<ClassTeacherDormViolationVO[]>> => {
return request({
url: '/classTeacherDorm/classTeacherDorm/violation/list',
method: 'get',
params: query
}).then((res) => unwrapResponse(res, '查询宿舍违纪'));
};
/** 本班宿舍点名列表 */
export const listClassTeacherRollCalls = (
query?: ClassTeacherDormQuery
): AxiosPromise<TableDataInfo<ClassTeacherDormRollCallVO[]>> => {
return request({
url: '/classTeacherDorm/classTeacherDorm/rollCall/list',
method: 'get',
params: query
}).then((res) => unwrapResponse(res, '查询宿舍点名'));
};
@@ -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 * @param hygieneCheckId 卫生检查ID
@@ -113,6 +113,32 @@ export interface DormHygieneCheckVO {
* 检查备注 * 检查备注
*/ */
checkRemarks: string; 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 { export interface DormHygieneCheckForm extends BaseEntity {
@@ -230,6 +256,21 @@ export interface DormHygieneCheckForm extends BaseEntity {
* 检查备注 * 检查备注
*/ */
checkRemarks?: string; checkRemarks?: string;
/**
* 违纪类型ID(兼容)
*/
disciplineTypeId?: string | number;
/**
* 违纪类型名称(兼容)
*/
disciplineTypeName?: string;
/**
* 扣分明细
*/
deductionItems?: HygieneDeductionItem[];
} }
export interface DormHygieneCheckQuery extends PageQuery { export interface DormHygieneCheckQuery extends PageQuery {
@@ -639,4 +680,9 @@ export interface DormHygieneCheckCompleteForm extends DormHygieneCheckForm {
* 学生评分数据列表 * 学生评分数据列表
*/ */
studentScores?: DormHygieneStudentScoreForm[]; studentScores?: DormHygieneStudentScoreForm[];
/**
* 扣分明细
*/
deductionItems?: HygieneDeductionItem[];
} }
@@ -0,0 +1,401 @@
<template>
<div class="p-2 page-container">
<el-card shadow="hover" class="mb-[10px]" v-loading="classLoading">
<div v-if="classInfo" class="class-info-bar">
<span class="class-info-label">所带班级:</span>
<el-tag type="primary" size="large">{{ classInfo.className }}</el-tag>
<span v-if="classInfo.collegeName" class="class-info-extra">{{ classInfo.collegeName }}</span>
<span v-if="classInfo.majorName" class="class-info-extra">{{ classInfo.majorName }}</span>
<span class="class-info-tip">(仅显示本班学生数据,按学生当前所在班级过滤)</span>
</div>
<el-alert
v-else-if="classLoadError"
type="error"
:closable="false"
show-icon
:title="classLoadError"
class="mb-0"
/>
<el-empty v-else-if="!classLoading" description="当前账号未绑定班主任班级" />
</el-card>
<el-alert
v-if="loadStatus"
:type="loadStatusType"
:closable="false"
show-icon
:title="loadStatus"
class="mb-[10px]"
/>
<el-alert
v-if="listLoadError"
type="warning"
:closable="false"
show-icon
:title="listLoadError"
class="mb-[10px]"
/>
<template v-if="pageReady">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="80px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="日期范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col v-if="activeTab === 'rollCall'" :span="5">
<el-form-item label="点名状态" prop="rollCallStatus">
<el-select v-model="queryParams.rollCallStatus" placeholder="全部状态" clearable style="width: 100%">
<el-option
v-for="dict in xg_dorm_roll_call_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="activeTab === 'rollCall' ? 6 : 6" class="search-btn-col">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<div class="flex justify-between items-center">
<span class="font-medium">班级宿舍情况</span>
<right-toolbar v-model:showSearch="showSearch" @queryTable="handleQuery" />
</div>
</template>
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="宿舍违纪" name="violation">
<el-table v-loading="violationLoading" border :data="violationList" empty-text="当前日期范围内暂无违纪记录,请尝试扩大日期范围">
<el-table-column label="学号" prop="studentNo" width="130" align="center" />
<el-table-column label="姓名" prop="studentName" width="100" align="center" />
<el-table-column label="来源" prop="dataSourceName" width="110" align="center" />
<el-table-column label="违纪类型" prop="violationTypeName" min-width="140" show-overflow-tooltip />
<el-table-column label="扣分" prop="scoreDeduction" width="80" align="center">
<template #default="scope">
<span v-if="scope.row.scoreDeduction != null">{{ scope.row.scoreDeduction }}</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column label="违纪日期" prop="violationDate" width="120" align="center">
<template #default="scope">
<span>{{ proxy?.parseTime(scope.row.violationDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="宿舍楼" prop="buildingName" width="120" show-overflow-tooltip />
<el-table-column label="房间" prop="roomName" width="100" align="center" />
<el-table-column label="违纪原因" prop="violationReason" min-width="160" show-overflow-tooltip />
<el-table-column label="状态" prop="status" width="80" align="center">
<template #default="scope">
<dict-tag :options="student_discipline_status" :value="scope.row.status" />
</template>
</el-table-column>
</el-table>
<pagination
v-show="violationTotal > 0"
v-model:page="violationQuery.pageNum"
v-model:limit="violationQuery.pageSize"
:total="violationTotal"
@pagination="getViolationList"
/>
</el-tab-pane>
<el-tab-pane label="宿舍点名" name="rollCall">
<el-table
v-loading="rollCallLoading"
border
:data="rollCallList"
:row-class-name="rollCallRowClassName"
empty-text="当前日期范围内暂无点名记录,请尝试扩大日期范围或调整筛选条件"
>
<el-table-column label="班级" prop="className" width="120" align="center" show-overflow-tooltip />
<el-table-column label="学号" prop="studentNo" width="130" align="center" />
<el-table-column label="姓名" prop="studentName" width="100" align="center" />
<el-table-column label="点名时间" prop="checkDate" width="170" align="center">
<template #default="scope">
<span>{{ proxy?.parseTime(scope.row.checkDate, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="点名状态" prop="rollCallStatus" width="100" align="center">
<template #default="scope">
<dict-tag :options="xg_dorm_roll_call_status" :value="scope.row.rollCallStatus" />
</template>
</el-table-column>
<el-table-column label="批次号" prop="batchNo" min-width="180" show-overflow-tooltip />
<el-table-column label="检查员" prop="checkerName" width="100" align="center" />
<el-table-column label="宿舍楼" prop="buildingName" width="120" show-overflow-tooltip />
<el-table-column label="房间" prop="roomName" width="100" align="center" />
<el-table-column label="床位" prop="bedCode" width="80" align="center" />
<el-table-column label="请假类型" prop="leaveTypeName" width="100" align="center" />
</el-table>
<pagination
v-show="rollCallTotal > 0"
v-model:page="rollCallQuery.pageNum"
v-model:limit="rollCallQuery.pageSize"
:total="rollCallTotal"
@pagination="getRollCallList"
/>
</el-tab-pane>
</el-tabs>
</el-card>
</template>
</div>
</template>
<script setup name="ClassTeacherDormView" lang="ts">
import { ref, reactive, onMounted, getCurrentInstance, type ComponentInternalInstance, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import {
getMyClassInfo,
listClassTeacherViolations,
listClassTeacherRollCalls,
type ClassTeacherClassInfo,
type ClassTeacherDormViolationVO,
type ClassTeacherDormRollCallVO
} from '@/api/xg/xgss/classTeacherDorm/index';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { student_discipline_status, xg_dorm_roll_call_status } = toRefs<any>(
proxy?.useDict('student_discipline_status', 'xg_dorm_roll_call_status')
);
const classLoading = ref(false);
const classInfo = ref<ClassTeacherClassInfo | null>(null);
const classLoadError = ref('');
const listLoadError = ref('');
const loadStatus = ref('');
const loadStatusType = ref<'success' | 'warning' | 'error' | 'info'>('info');
const pageReady = ref(false);
const showSearch = ref(true);
const activeTab = ref('violation');
const queryFormRef = ref();
const formatDate = (date: Date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
};
const today = () => formatDate(new Date());
const defaultDateRange = (): [string, string] => [today(), today()];
const dateRange = ref<[string, string]>(defaultDateRange());
const queryParams = reactive({
studentNo: '',
studentName: '',
rollCallStatus: ''
});
const violationLoading = ref(false);
const violationList = ref<ClassTeacherDormViolationVO[]>([]);
const violationTotal = ref(0);
const violationQuery = reactive({ pageNum: 1, pageSize: 10 });
const rollCallLoading = ref(false);
const rollCallList = ref<ClassTeacherDormRollCallVO[]>([]);
const rollCallTotal = ref(0);
const rollCallQuery = reactive({ pageNum: 1, pageSize: 10 });
const buildDateParams = () => ({
beginDate: dateRange.value?.[0] || defaultDateRange()[0],
endDate: dateRange.value?.[1] || today(),
studentNo: queryParams.studentNo || undefined,
studentName: queryParams.studentName || undefined,
rollCallStatus: activeTab.value === 'rollCall' && queryParams.rollCallStatus ? queryParams.rollCallStatus : undefined
});
/** 不在寝(status=2)整行高亮 */
const rollCallRowClassName = ({ row }: { row: ClassTeacherDormRollCallVO }) => {
return row.rollCallStatus === '2' ? 'roll-call-absent-row' : '';
};
const extractRows = (res: any) => res?.rows ?? res?.data?.rows ?? [];
const extractTotal = (res: any) => Number(res?.total ?? res?.data?.total ?? 0);
const extractErrorMsg = (error: any) => error?.message || error?.response?.data?.msg || '请求失败,请稍后重试';
const updateLoadStatus = () => {
const range = `${dateRange.value?.[0] || ''} ~ ${dateRange.value?.[1] || ''}`;
if (classLoadError.value || listLoadError.value) {
loadStatusType.value = 'error';
loadStatus.value = [classLoadError.value, listLoadError.value].filter(Boolean).join(';');
return;
}
loadStatusType.value = 'success';
loadStatus.value = `查询完成(${range}):班级「${classInfo.value?.className || '未知'}」,违纪 ${violationTotal.value} 条,点名 ${rollCallTotal.value} 条`;
};
const loadClassInfo = async () => {
classLoading.value = true;
classLoadError.value = '';
try {
const res: any = await getMyClassInfo();
classInfo.value = res?.data ?? null;
if (!classInfo.value?.className) {
classLoadError.value = res?.msg || '未获取到班级信息,请确认当前账号已绑定班主任班级';
ElMessage.error(classLoadError.value);
}
} catch (error: any) {
classInfo.value = null;
classLoadError.value = extractErrorMsg(error);
ElMessage.error(classLoadError.value);
} finally {
classLoading.value = false;
updateLoadStatus();
}
};
const getViolationList = async () => {
violationLoading.value = true;
listLoadError.value = '';
try {
const res: any = await listClassTeacherViolations({
...buildDateParams(),
pageNum: violationQuery.pageNum,
pageSize: violationQuery.pageSize
});
violationList.value = extractRows(res);
violationTotal.value = extractTotal(res);
} catch (error: any) {
violationList.value = [];
violationTotal.value = 0;
listLoadError.value = `宿舍违纪:${extractErrorMsg(error)}`;
ElMessage.error(listLoadError.value);
} finally {
violationLoading.value = false;
updateLoadStatus();
}
};
const getRollCallList = async () => {
rollCallLoading.value = true;
listLoadError.value = '';
try {
const res: any = await listClassTeacherRollCalls({
...buildDateParams(),
pageNum: rollCallQuery.pageNum,
pageSize: rollCallQuery.pageSize
});
rollCallList.value = extractRows(res);
rollCallTotal.value = extractTotal(res);
} catch (error: any) {
rollCallList.value = [];
rollCallTotal.value = 0;
listLoadError.value = `宿舍点名:${extractErrorMsg(error)}`;
ElMessage.error(listLoadError.value);
} finally {
rollCallLoading.value = false;
updateLoadStatus();
}
};
const handleQuery = () => {
violationQuery.pageNum = 1;
rollCallQuery.pageNum = 1;
if (activeTab.value === 'violation') {
getViolationList();
} else {
getRollCallList();
}
};
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.studentNo = '';
queryParams.studentName = '';
queryParams.rollCallStatus = '';
dateRange.value = defaultDateRange();
handleQuery();
};
const handleTabChange = (name: string | number) => {
if (name === 'violation') {
getViolationList();
} else {
getRollCallList();
}
};
onMounted(async () => {
await loadClassInfo();
pageReady.value = true;
await getViolationList();
});
</script>
<style scoped>
.page-container {
min-height: calc(100vh - 120px);
}
.class-info-bar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.class-info-label {
font-weight: 600;
color: var(--el-text-color-primary);
}
.class-info-extra {
color: var(--el-text-color-secondary);
font-size: 14px;
}
.class-info-tip {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.search-btn-col {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding-top: 4px;
}
:deep(.roll-call-absent-row) {
background-color: #fef0f0 !important;
}
:deep(.roll-call-absent-row:hover > td) {
background-color: #fde2e2 !important;
}
</style>
@@ -201,8 +201,8 @@
<el-table-column prop="studentNo" label="学号" width="120" /> <el-table-column prop="studentNo" label="学号" width="120" />
<el-table-column prop="gender" label="性别" width="60"> <el-table-column prop="gender" label="性别" width="60">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.gender === '0'" type="primary" size="small">男</el-tag> <el-tag v-if="isMaleGender(scope.row.gender)" type="primary" size="small">男</el-tag>
<el-tag v-else-if="scope.row.gender === '1'" class="female-tag" size="small">女</el-tag> <el-tag v-else-if="isFemaleGender(scope.row.gender)" class="female-tag" size="small">女</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="collegeName" label="学院名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="collegeName" label="学院名称" min-width="120" show-overflow-tooltip />
@@ -356,6 +356,7 @@ import { getToken } from '@/utils/auth';
import ImagePreview from '@/components/ImagePreview/index.vue'; import ImagePreview from '@/components/ImagePreview/index.vue';
import request from '@/utils/request'; import request from '@/utils/request';
import { DormHygieneCheckForm, DormHygieneCheckCompleteForm } from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types'; import { DormHygieneCheckForm, DormHygieneCheckCompleteForm } from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types';
import { isFemaleGender, isMaleGender, mergeGenderFromRoomStudents, normalizeGender } from './genderUtils';
// 暂时注释掉用户信息获取,避免linter错误 // 暂时注释掉用户信息获取,避免linter错误
// import { getInfo } from '@/api/login'; // import { getInfo } from '@/api/login';
@@ -1035,7 +1036,7 @@ const loadRoomStudents = async () => {
console.log('获取到历史学生评分数据:', studentScoresRes.data); console.log('获取到历史学生评分数据:', studentScoresRes.data);
// 将历史评分数据转换为表格显示格式 // 将历史评分数据转换为表格显示格式
roomStudents.value = studentScoresRes.data.map((studentScore: any) => ({ let mappedStudents = studentScoresRes.data.map((studentScore: any) => ({
// 保存学生评分记录的ID,用于编辑时提交 // 保存学生评分记录的ID,用于编辑时提交
id: studentScore.id, id: studentScore.id,
studentId: studentScore.studentId, studentId: studentScore.studentId,
@@ -1047,7 +1048,7 @@ const loadRoomStudents = async () => {
bedId: studentScore.bedId, bedId: studentScore.bedId,
bedName: studentScore.bedCode, // 使用bedCode作为bedName显示 bedName: studentScore.bedCode, // 使用bedCode作为bedName显示
bedCode: studentScore.bedCode, bedCode: studentScore.bedCode,
gender: studentScore.gender || '1', // 默认女性,如果没有性别字段 gender: normalizeGender(studentScore.gender),
// 使用历史评分数据 // 使用历史评分数据
student_score: studentScore.studentScore || null, student_score: studentScore.studentScore || null,
attendance_status: studentScore.attendanceStatus || '1', attendance_status: studentScore.attendanceStatus || '1',
@@ -1057,6 +1058,20 @@ const loadRoomStudents = async () => {
improvementNotes: studentScore.improvementNotes || '' 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); console.log('转换后的学生数据:', roomStudents.value);
return; return;
} else { } else {
@@ -1076,6 +1091,7 @@ const loadRoomStudents = async () => {
// 为每个学生设置默认值 // 为每个学生设置默认值
roomStudents.value = res.data.map((student: any) => ({ roomStudents.value = res.data.map((student: any) => ({
...student, ...student,
gender: normalizeGender(student.gender),
student_score: 0, // 个人得分,新增时默认为0 student_score: 0, // 个人得分,新增时默认为0
attendance_status: student.attendance_status || '1', // 出勤状态,默认1-在寝 attendance_status: student.attendance_status || '1', // 出勤状态,默认1-在寝
student_grade: student.student_grade || 'A' // 学生评级,默认A-优秀 student_grade: student.student_grade || 'A' // 学生评级,默认A-优秀
@@ -47,30 +47,16 @@
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="6"> <el-col :span="24">
<el-form-item label="违纪类型" prop="disciplineTypeId" class="discipline-form-item"> <DisciplineDeductionMultiSelect
<el-select v-model="deductionItems"
v-model="hygieneForm.disciplineTypeId" :options="disciplineTypeOptions"
placeholder="请选择违纪类型" @update:total-score="handleDeductionTotalChange"
style="width: 100%" />
@change="handleDisciplineTypeChange"
>
<el-option
v-for="item in disciplineTypeOptions"
:key="item.id"
:label="item.typeName"
:value="item.id"
:data-score="item.minScore"
/>
</el-select>
<!-- 违纪类型说明展示 -->
<div v-if="hygieneForm.disciplineTypeId && selectedDisciplineTypeRemark" class="discipline-type-remark">
<el-icon class="remark-icon"><InfoFilled /></el-icon>
<span class="remark-text">类型说明:{{ selectedDisciplineTypeRemark }}</span>
</div>
</el-form-item>
</el-col> </el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="宿舍扣分" prop="totalScore"> <el-form-item label="宿舍扣分" prop="totalScore">
<el-input-number <el-input-number
@@ -78,9 +64,9 @@
:min="0" :min="0"
:max="100" :max="100"
:precision="2" :precision="2"
placeholder="请输入扣分" placeholder="自动合计"
style="width: 100%" style="width: 100%"
@change="handleTotalScoreChange" disabled
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -193,8 +179,8 @@
<div class="student-basic-info"> <div class="student-basic-info">
<div class="student-name-with-gender"> <div class="student-name-with-gender">
<h4>{{ student.studentName }}</h4> <h4>{{ student.studentName }}</h4>
<el-tag v-if="student.gender === '0'" type="primary" size="small">男</el-tag> <el-tag v-if="isMaleGender(student.gender)" type="primary" size="small">男</el-tag>
<el-tag v-else class="female-tag" size="small">女</el-tag> <el-tag v-else-if="isFemaleGender(student.gender)" class="female-tag" size="small">女</el-tag>
</div> </div>
<p>{{ student.studentNo }}</p> <p>{{ student.studentNo }}</p>
</div> </div>
@@ -368,6 +354,11 @@ import request from '@/utils/request';
import ViolationRecordDialog from './components/ViolationRecordDialog.vue'; import ViolationRecordDialog from './components/ViolationRecordDialog.vue';
// @ts-expect-error - Component import without type declarations // @ts-expect-error - Component import without type declarations
import LeaveProcessDialog from './components/LeaveProcessDialog.vue'; 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; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -396,6 +387,7 @@ const studentsLoading = ref(false);
const violationsLoading = ref(false); const violationsLoading = ref(false);
const attachmentList = ref<any[]>([]); const attachmentList = ref<any[]>([]);
const disciplineTypeOptions = ref<any[]>([]); const disciplineTypeOptions = ref<any[]>([]);
const deductionItems = ref<HygieneDeductionItem[]>([]);
// 宿舍卫生检查表单 // 宿舍卫生检查表单
const hygieneForm = reactive({ const hygieneForm = reactive({
@@ -449,8 +441,8 @@ const hygieneRules = reactive({
// 学生评分数据 // 学生评分数据
const studentScores = ref<any[]>([]); const studentScores = ref<any[]>([]);
// 违纪记录数据 // 违纪记录数据(使用普通对象,避免 Map 变更无法触发 Vue 响应式更新)
const violationRecords = ref<Map<string | number, any[]>>(new Map()); const violationRecords = ref<Record<string, any[]>>({});
// 学生请假数据 // 学生请假数据
const studentLeaves = ref<Map<string | number, any>>(new Map()); const studentLeaves = ref<Map<string | number, any>>(new Map());
@@ -491,14 +483,10 @@ const violationDialogTitle = computed(() => {
return currentStudent.value ? `${currentStudent.value.studentName} - 违纪记录录入` : '违纪记录录入'; return currentStudent.value ? `${currentStudent.value.studentName} - 违纪记录录入` : '违纪记录录入';
}); });
// 获取违纪类型说明 // 扣分明细合计同步到宿舍扣分
const selectedDisciplineTypeRemark = computed(() => { const handleDeductionTotalChange = (total: number) => {
if (hygieneForm.disciplineTypeId) { hygieneForm.totalScore = total;
const selectedType = disciplineTypeOptions.value.find((item) => item.id === hygieneForm.disciplineTypeId); };
return selectedType ? selectedType.remark : '';
}
return '';
});
// 上传配置 // 上传配置
const uploadAction = computed(() => { const uploadAction = computed(() => {
@@ -558,24 +546,26 @@ const loadDisciplineTypes = async () => {
} }
}; };
// 处理违纪类型变化 // 初始化扣分明细
const handleDisciplineTypeChange = () => { const initDeductionItems = (data?: any) => {
if (hygieneForm.disciplineTypeId) { if (data?.deductionItems && data.deductionItems.length > 0) {
const selectedType = disciplineTypeOptions.value.find((item) => item.id === hygieneForm.disciplineTypeId); deductionItems.value = data.deductionItems.map((item: HygieneDeductionItem, index: number) => ({
if (selectedType) { ...item,
hygieneForm.disciplineTypeName = selectedType.typeName; sortOrder: item.sortOrder ?? index + 1
// 自动填入最低分 }));
if (selectedType.minScore !== null && selectedType.minScore !== undefined) { } else if (data?.disciplineTypeId) {
const minScore = parseFloat(selectedType.minScore); deductionItems.value = [
if (!isNaN(minScore)) { {
hygieneForm.totalScore = minScore; disciplineTypeId: data.disciplineTypeId,
ElMessage.success(`已自动填入最低扣分:${minScore}分`); disciplineTypeName: data.disciplineTypeName,
} score: data.totalScore ?? 0,
sortOrder: 1
} }
} ];
} else { } else {
hygieneForm.disciplineTypeName = ''; deductionItems.value = [];
} }
hygieneForm.totalScore = deductionItems.value.reduce((sum, item) => sum + (Number(item.score) || 0), 0);
}; };
// 初始化表单数据(已简化,主要逻辑移到watch中) // 初始化表单数据(已简化,主要逻辑移到watch中)
@@ -594,32 +584,48 @@ const initFormData = async () => {
// 加载学生评分数据 // 加载学生评分数据
const loadStudentScores = async () => { const loadStudentScores = async () => {
console.log('loadStudentScores 方法被调用,hygieneForm.id:', hygieneForm.id); const checkId = normalizeId(hygieneForm.id);
if (!checkId) {
if (!hygieneForm.id) {
console.log('没有检查ID,跳过学生数据加载'); console.log('没有检查ID,跳过学生数据加载');
return; return;
} }
console.log('开始加载学生评分数据,检查ID:', hygieneForm.id); console.log('开始加载学生评分数据,检查ID:', checkId);
studentsLoading.value = true; studentsLoading.value = true;
try { try {
const res = await request({ const res = await request({
url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${hygieneForm.id}`, url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${checkId}`,
method: 'get' 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) { const missingGender = scores.some((student: any) => !student.gender);
studentScores.value = res.data; if (missingGender && hygieneForm.roomId) {
console.log('学生评分数据加载成功:', res.data.length, '条'); 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);
}
}
// 同时加载违纪记录和请假信息 studentScores.value = scores;
await Promise.all([loadViolationRecords(), loadStudentLeaves()]); console.log('学生评分数据加载成功:', scores.length, '条');
await loadStudentLeaves();
} else { } else {
studentScores.value = []; studentScores.value = [];
console.log('学生评分数据为空,响应码:', res.code);
} }
} catch (error) { } catch (error) {
console.error('加载学生评分数据失败:', error); console.error('加载学生评分数据失败:', error);
@@ -631,29 +637,55 @@ const loadStudentScores = async () => {
}; };
// 加载违纪记录数据 // 加载违纪记录数据
const loadViolationRecords = async () => { const applyViolationList = (list: any[]) => {
if (!hygieneForm.id) return; const grouped: Record<string, any[]> = {};
(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 { try {
const res = await request({ const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${hygieneForm.id}`, url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${checkId}`,
method: 'get' method: 'get'
}); });
if (res.code === 200 && res.data) { if (res.code === 200 && Array.isArray(res.data)) {
// 重新组织违纪记录数据,按学生ID分组 applyViolationList(res.data);
violationRecords.value.clear(); console.log('违纪记录加载成功:', res.data.length, '条');
res.data.forEach((violation: any) => { } else {
const studentId = violation.studentId; violationRecords.value = {};
if (!violationRecords.value.has(studentId)) {
violationRecords.value.set(studentId, []);
}
violationRecords.value.get(studentId)?.push(violation);
});
} }
} catch (error) { } catch (error) {
console.error('加载违纪记录数据失败:', error); console.error('加载违纪记录数据失败:', error);
ElMessage.error('加载违纪记录数据失败'); ElMessage.error('加载违纪记录数据失败');
} finally {
violationsLoading.value = false;
} }
}; };
@@ -675,9 +707,9 @@ const loadStudentLeaves = async () => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
studentLeaves.value.clear(); studentLeaves.value.clear();
Object.keys(res.data).forEach((studentId) => { Object.keys(res.data).forEach((studentId) => {
studentLeaves.value.set(Number(studentId), res.data[studentId]); const normalizedId = normalizeStudentId(studentId);
// 如果学生有请假信息,自动设置出勤状态为3(请假) studentLeaves.value.set(normalizedId, res.data[studentId]);
const student = studentScores.value.find((s) => s.studentId === Number(studentId)); const student = studentScores.value.find((s) => normalizeStudentId(s.studentId) === normalizedId);
if (student) { if (student) {
student.attendanceStatus = '3'; student.attendanceStatus = '3';
} }
@@ -691,12 +723,31 @@ const loadStudentLeaves = async () => {
// 获取学生的违纪记录 // 获取学生的违纪记录
const getStudentViolations = (studentId: string | number) => { 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) => { 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 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]; const violation = violations[index];
// 检查是否为已撤销的违纪记录 // 检查是否为已撤销的违纪记录
@@ -851,11 +903,14 @@ const removeViolationRecord = async (studentId: string | number, index: number)
violations.splice(index, 1); violations.splice(index, 1);
const nextRecords = { ...violationRecords.value };
if (violations.length === 0) { if (violations.length === 0) {
violationRecords.value.delete(studentId); delete nextRecords[normalizedId];
} else { } else {
violationRecords.value.set(studentId, violations); nextRecords[normalizedId] = violations;
} }
violationRecords.value = nextRecords;
syncStudentScoreFromViolations(normalizedId);
if (window.ElMessage) { if (window.ElMessage) {
window.ElMessage.success('违纪记录删除成功'); window.ElMessage.success('违纪记录删除成功');
@@ -867,23 +922,32 @@ const removeViolationRecord = async (studentId: string | number, index: number)
// 违纪记录确认 // 违纪记录确认
const handleViolationConfirm = (violationData: any) => { const handleViolationConfirm = (violationData: any) => {
const studentId = currentStudent.value.studentId; const studentId = normalizeStudentId(currentStudent.value.studentId);
const violations = violationRecords.value.get(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; violations[currentViolationIndex.value] = violationData;
if (window.ElMessage) {
window.ElMessage.success('违纪记录保存成功');
}
} else { } else {
// 新增模式
violations.push(violationData); 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; violationDialogVisible.value = false;
if (window.ElMessage) {
window.ElMessage.success('违纪记录保存成功');
}
}; };
// 违纪记录弹窗关闭 // 违纪记录弹窗关闭
@@ -1110,8 +1174,16 @@ const submitForm = async () => {
try { try {
await hygieneFormRef.value?.validate(); await hygieneFormRef.value?.validate();
if ((hygieneForm.totalScore || 0) > 100) {
ElMessage.error('宿舍合计扣分不能超过100分');
return;
}
buttonLoading.value = true; buttonLoading.value = true;
// 提交前同步个人扣分为违纪合计
syncStudentScoreFromViolations();
// 处理附件数据 // 处理附件数据
const attachmentOssIds = attachmentList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId); const attachmentOssIds = attachmentList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId);
hygieneForm.attachmentIds = attachmentOssIds.join(','); hygieneForm.attachmentIds = attachmentOssIds.join(',');
@@ -1138,12 +1210,30 @@ const submitForm = async () => {
// 处理违纪记录数据 // 处理违纪记录数据
const violationData: any[] = []; 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) => { violations.forEach((violation) => {
violationData.push({ violationData.push({
...violation, disciplineRecordId:
studentId, violation.disciplineRecordId != null ? normalizeId(violation.disciplineRecordId) : undefined,
violationSource: '1' // 宿舍卫生检查来源 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 = { const submitData = {
...hygieneForm, ...hygieneForm,
deductionItems: deductionItems.value,
studentScores: studentScoresData, studentScores: studentScoresData,
violationData violationData
}; };
@@ -1171,44 +1262,67 @@ const handleClose = () => {
emit('close'); emit('close');
}; };
// 监听弹窗显示状态 const detailLoadToken = ref(0);
watch(
() => props.visible, const syncFormFromProps = (data?: any) => {
async (visible, oldVisible) => { if (!data) return;
console.log('弹窗显示状态变化:', oldVisible, '->', visible); Object.assign(hygieneForm, {
if (visible) { ...data,
console.log('弹窗打开,开始初始化表单数据'); id: data.id != null ? normalizeId(data.id) : undefined,
// 只加载违纪类型数据,其他数据通过formData的watch来加载 roomId: data.roomId != null ? normalizeId(data.roomId) : undefined
await loadDisciplineTypes(); });
} else { initDeductionItems(data);
console.log('弹窗关闭'); };
}
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( watch(
() => props.formData, () => [props.visible, normalizeId(props.formData?.id)] as const,
async (newData, oldData) => { async ([visible, checkId]) => {
console.log('props.formData 变化:', oldData, '->', newData); if (!visible) {
if (newData) { detailLoadToken.value += 1;
console.log('更新表单数据'); violationRecords.value = {};
Object.assign(hygieneForm, newData); studentScores.value = [];
console.log('表单数据更新后,hygieneForm.id:', hygieneForm.id); hygieneForm.id = undefined;
return;
// 如果弹窗是打开状态且有ID,需要重新加载相关数据
if (props.visible && hygieneForm.id) {
console.log('检测到有检查ID,重新加载学生数据和附件数据...');
try {
await Promise.all([loadStudentScores(), handleAttachmentData()]);
console.log('学生数据和附件数据重新加载完成');
} catch (error) {
console.error('重新加载数据失败:', error);
}
}
} }
if (!checkId) return;
await loadDisciplineTypes();
await nextTick();
await loadCheckDetailData();
}, },
{ deep: true, immediate: true } { immediate: true }
); );
</script> </script>
@@ -201,12 +201,13 @@
</template> </template>
<script setup name="DormHygieneCheckMainDialog" lang="ts"> <script setup name="DormHygieneCheckMainDialog" lang="ts">
import { ref, reactive, computed, watch, getCurrentInstance, ComponentInternalInstance } from 'vue'; import { ref, reactive, computed, watch, getCurrentInstance, ComponentInternalInstance, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { Check, Calendar } from '@element-plus/icons-vue'; import { Check, Calendar } from '@element-plus/icons-vue';
import DormHygieneCheckSingleDialog from './DormHygieneCheckSingleDialog.vue'; import DormHygieneCheckSingleDialog from './DormHygieneCheckSingleDialog.vue';
import DormHygieneCheckEditDialog from './DormHygieneCheckEditDialog.vue'; import DormHygieneCheckEditDialog from './DormHygieneCheckEditDialog.vue';
import request from '@/utils/request'; import request from '@/utils/request';
import { normalizeId } from './studentIdUtils';
// 组件实例 // 组件实例
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -531,7 +532,7 @@ const handleRoomDoubleClick = async (room: any) => {
} }
// 检查是否已有记录ID(新增成功后会保存ID) // 检查是否已有记录ID(新增成功后会保存ID)
const checkId = roomCheckIds.value.get(room.id); const checkId = normalizeId(roomCheckIds.value.get(room.id));
if (checkId) { if (checkId) {
// 有记录ID,说明已经检查过,打开编辑弹窗 // 有记录ID,说明已经检查过,打开编辑弹窗
@@ -542,8 +543,22 @@ const handleRoomDoubleClick = async (room: any) => {
}); });
if (response.code === 200 && response.data) { if (response.code === 200 && response.data) {
let editPayload: Record<string, any> = {
...response.data,
id: checkId
};
try {
const violationsRes = await request({
url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${checkId}`,
method: 'get'
});
editPayload._prefetchedViolations = violationsRes.data || [];
} catch (error) {
console.warn('预加载违纪记录失败,将在弹窗内重试', error);
}
currentRoom.value = room; currentRoom.value = room;
currentEditData.value = response.data; currentEditData.value = editPayload;
await nextTick();
editCheckVisible.value = true; editCheckVisible.value = true;
return; return;
} }
@@ -570,7 +585,7 @@ const handleSingleCheckSubmit = (data: any) => {
// 保存检查记录ID(如果有返回) // 保存检查记录ID(如果有返回)
if (data.id) { if (data.id) {
roomCheckIds.value.set(currentRoom.value.id, data.id); roomCheckIds.value.set(currentRoom.value.id, normalizeId(data.id));
} }
ElMessage.success(`${currentRoom.value.roomName} 检查完成`); ElMessage.success(`${currentRoom.value.roomName} 检查完成`);
@@ -595,7 +610,7 @@ const handleEditCheckSubmit = async (data: any) => {
if (response.code === 200) { if (response.code === 200) {
// 标记房间为已完成 // 标记房间为已完成
completedRooms.value.add(currentRoom.value.id); completedRooms.value.add(currentRoom.value.id);
roomCheckIds.value.set(currentRoom.value.id, data.id); roomCheckIds.value.set(currentRoom.value.id, normalizeId(data.id));
ElMessage.success(`${currentRoom.value.roomName} 检查记录更新完成`); ElMessage.success(`${currentRoom.value.roomName} 检查记录更新完成`);
editCheckVisible.value = false; editCheckVisible.value = false;
@@ -12,30 +12,16 @@
<el-form ref="hygieneFormRef" :model="hygieneForm" :rules="hygieneRules" label-width="120px"> <el-form ref="hygieneFormRef" :model="hygieneForm" :rules="hygieneRules" label-width="120px">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="24">
<el-form-item label="违纪类型" prop="disciplineTypeId" class="discipline-form-item"> <DisciplineDeductionMultiSelect
<el-select v-model="deductionItems"
v-model="hygieneForm.disciplineTypeId" :options="disciplineTypeOptions"
placeholder="请选择违纪类型" @update:total-score="handleDeductionTotalChange"
style="width: 100%" />
@change="handleDisciplineTypeChange"
>
<el-option
v-for="item in disciplineTypeOptions"
:key="item.id"
:label="item.typeName"
:value="item.id"
:data-score="item.minScore"
/>
</el-select>
<!-- 违纪类型说明展示 -->
<div v-if="hygieneForm.disciplineTypeId && selectedDisciplineTypeRemark" class="discipline-type-remark">
<el-icon class="remark-icon"><InfoFilled /></el-icon>
<span class="remark-text">类型说明:{{ selectedDisciplineTypeRemark }}</span>
</div>
</el-form-item>
</el-col> </el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="宿舍扣分" prop="totalScore"> <el-form-item label="宿舍扣分" prop="totalScore">
<el-input-number <el-input-number
@@ -43,9 +29,9 @@
:min="0" :min="0"
:max="100" :max="100"
:precision="2" :precision="2"
placeholder="请输入扣分" placeholder="自动合计"
style="width: 100%" style="width: 100%"
@change="handleTotalScoreChange" disabled
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -151,8 +137,8 @@
<div class="student-basic-info"> <div class="student-basic-info">
<div class="student-name-with-gender"> <div class="student-name-with-gender">
<h4>{{ student.studentName }}</h4> <h4>{{ student.studentName }}</h4>
<el-tag v-if="student.gender === '0'" type="primary" size="small">男</el-tag> <el-tag v-if="isMaleGender(student.gender)" type="primary" size="small">男</el-tag>
<el-tag v-else class="female-tag" size="small">女</el-tag> <el-tag v-else-if="isFemaleGender(student.gender)" class="female-tag" size="small">女</el-tag>
</div> </div>
<p>{{ student.studentNo }}</p> <p>{{ student.studentNo }}</p>
</div> </div>
@@ -311,6 +297,11 @@ import request from '@/utils/request';
import ViolationRecordDialog from './components/ViolationRecordDialog.vue'; import ViolationRecordDialog from './components/ViolationRecordDialog.vue';
// @ts-expect-error - Component import without type declarations // @ts-expect-error - Component import without type declarations
import LeaveProcessDialog from './components/LeaveProcessDialog.vue'; 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 } from './genderUtils';
import { normalizeStudentId } from './studentIdUtils';
import { calcStudentViolationScoreTotal } from './violationScoreUtils';
// 组件实例 // 组件实例
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -339,6 +330,7 @@ const buttonLoading = ref(false);
const studentsLoading = ref(false); const studentsLoading = ref(false);
const attachmentList = ref<any[]>([]); const attachmentList = ref<any[]>([]);
const disciplineTypeOptions = ref<any[]>([]); const disciplineTypeOptions = ref<any[]>([]);
const deductionItems = ref<HygieneDeductionItem[]>([]);
// 宿舍卫生检查表单 // 宿舍卫生检查表单
const hygieneForm = reactive({ const hygieneForm = reactive({
@@ -448,21 +440,8 @@ const loadDisciplineTypes = async () => {
} }
}; };
// 处理违纪类型变化 const handleDeductionTotalChange = (total: number) => {
const handleDisciplineTypeChange = () => { hygieneForm.totalScore = total;
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) {
hygieneForm.totalScore = selectedType.minScore;
ElMessage.success(`已自动填入最低扣分:${selectedType.minScore}分`);
}
}
} else {
hygieneForm.disciplineTypeName = '';
}
}; };
// 加载房间学生信息 // 加载房间学生信息
@@ -483,6 +462,7 @@ const loadRoomStudents = async () => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
roomStudents.value = res.data.map((student: any) => ({ roomStudents.value = res.data.map((student: any) => ({
...student, ...student,
studentId: normalizeStudentId(student.studentId),
student_score: 0, student_score: 0,
attendance_status: '1', attendance_status: '1',
student_grade: 'A' student_grade: 'A'
@@ -520,9 +500,9 @@ const loadStudentLeaves = async () => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
studentLeaves.value.clear(); studentLeaves.value.clear();
Object.keys(res.data).forEach((studentId) => { Object.keys(res.data).forEach((studentId) => {
studentLeaves.value.set(Number(studentId), res.data[studentId]); const normalizedId = normalizeStudentId(studentId);
// 如果学生有请假信息,自动设置出勤状态为3(请假) studentLeaves.value.set(normalizedId, res.data[studentId]);
const student = roomStudents.value.find((s) => s.studentId === Number(studentId)); const student = roomStudents.value.find((s) => normalizeStudentId(s.studentId) === normalizedId);
if (student) { if (student) {
student.attendance_status = '3'; student.attendance_status = '3';
} }
@@ -534,23 +514,32 @@ const loadStudentLeaves = async () => {
} }
}; };
// 获取违纪类型说明
const selectedDisciplineTypeRemark = computed(() => {
if (hygieneForm.disciplineTypeId) {
const selectedType = disciplineTypeOptions.value.find((item) => item.id === hygieneForm.disciplineTypeId);
return selectedType ? selectedType.remark : '';
}
return '';
});
// 获取学生的违纪记录 // 获取学生的违纪记录
const getStudentViolations = (studentId: string | number) => { const getStudentViolations = (studentId: string | number) => {
return violationRecords.value.get(studentId) || []; return violationRecords.value.get(normalizeStudentId(studentId)) || [];
};
const syncStudentScoreFromViolations = (studentId?: string | number) => {
const syncOne = (sid: string) => {
const student = roomStudents.value.find((item) => normalizeStudentId(item.studentId) === sid);
if (student) {
student.student_score = calcStudentViolationScoreTotal(getStudentViolations(sid));
}
};
if (studentId != null && studentId !== '') {
syncOne(normalizeStudentId(studentId));
return;
}
roomStudents.value.forEach((student) => {
syncOne(normalizeStudentId(student.studentId));
});
}; };
// 获取学生的请假信息 // 获取学生的请假信息
const getStudentLeave = (studentId: string | number) => { const getStudentLeave = (studentId: string | number) => {
return studentLeaves.value.get(studentId) || null; return studentLeaves.value.get(normalizeStudentId(studentId)) || null;
}; };
// 格式化日期时间 // 格式化日期时间
@@ -584,14 +573,16 @@ const removeViolationRecord = async (studentId: string | number, index: number)
type: 'warning' type: 'warning'
}); });
const violations = violationRecords.value.get(studentId) || []; const normalizedId = normalizeStudentId(studentId);
const violations = violationRecords.value.get(normalizedId) || [];
violations.splice(index, 1); violations.splice(index, 1);
if (violations.length === 0) { if (violations.length === 0) {
violationRecords.value.delete(studentId); violationRecords.value.delete(normalizedId);
} else { } else {
violationRecords.value.set(studentId, violations); violationRecords.value.set(normalizedId, violations);
} }
syncStudentScoreFromViolations(normalizedId);
ElMessage.success('违纪记录删除成功'); ElMessage.success('违纪记录删除成功');
} catch { } catch {
@@ -601,21 +592,23 @@ const removeViolationRecord = async (studentId: string | number, index: number)
// 违纪记录确认 // 违纪记录确认
const handleViolationConfirm = (violationData: any) => { const handleViolationConfirm = (violationData: any) => {
const studentId = currentStudent.value.studentId; const studentId = normalizeStudentId(currentStudent.value.studentId);
const violations = violationRecords.value.get(studentId) || []; const violations = violationRecords.value.get(studentId) || [];
if (currentViolationIndex.value >= 0) { if (Array.isArray(violationData)) {
// 编辑模式 violations.push(...violationData);
ElMessage.success(`已添加 ${violationData.length} 条违纪记录`);
} else if (currentViolationIndex.value >= 0) {
violations[currentViolationIndex.value] = violationData; violations[currentViolationIndex.value] = violationData;
ElMessage.success('违纪记录保存成功');
} else { } else {
// 新增模式
violations.push(violationData); violations.push(violationData);
ElMessage.success('违纪记录保存成功');
} }
violationRecords.value.set(studentId, violations); violationRecords.value.set(studentId, violations);
syncStudentScoreFromViolations(studentId);
violationDialogVisible.value = false; violationDialogVisible.value = false;
ElMessage.success('违纪记录保存成功');
}; };
// 违纪记录弹窗关闭 // 违纪记录弹窗关闭
@@ -880,8 +873,15 @@ const submitForm = async () => {
try { try {
await hygieneFormRef.value?.validate(); await hygieneFormRef.value?.validate();
if ((hygieneForm.totalScore || 0) > 100) {
ElMessage.error('宿舍合计扣分不能超过100分');
return;
}
buttonLoading.value = true; buttonLoading.value = true;
syncStudentScoreFromViolations();
// 处理附件数据 // 处理附件数据
const attachmentOssIds = attachmentList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId); const attachmentOssIds = attachmentList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId);
hygieneForm.attachmentIds = attachmentOssIds.join(','); hygieneForm.attachmentIds = attachmentOssIds.join(',');
@@ -938,6 +938,7 @@ const submitForm = async () => {
// 宿舍整体评分 // 宿舍整体评分
...hygieneForm, ...hygieneForm,
deductionItems: deductionItems.value,
// 学生评分数据 // 学生评分数据
studentScores, studentScores,
@@ -1003,6 +1004,7 @@ const resetForm = () => {
checkRemarks: '', checkRemarks: '',
attachmentIds: '' attachmentIds: ''
}); });
deductionItems.value = [];
roomStudents.value = []; roomStudents.value = [];
violationRecords.value.clear(); violationRecords.value.clear();
@@ -0,0 +1,231 @@
<template>
<div class="deduction-multi-select">
<el-form-item label="扣分项" class="discipline-form-item">
<el-select
v-if="!disabled"
v-model="selectedTypeIds"
multiple
collapse-tags
collapse-tags-tooltip
filterable
clearable
placeholder="请选择扣分项(可多选)"
style="width: 100%"
@change="handleTypeSelectionChange"
>
<el-option v-for="item in options" :key="item.id" :label="formatOptionLabel(item)" :value="item.id" />
</el-select>
<div v-else-if="!items.length" class="empty-text">无扣分项</div>
</el-form-item>
<div v-if="items.length > 0" class="deduction-list">
<div v-for="(item, index) in items" :key="item.disciplineTypeId" class="deduction-item">
<div class="deduction-item-main">
<span class="deduction-name">{{ item.disciplineTypeName }}</span>
<el-input-number
v-model="item.score"
:min="getMinScore(item)"
:max="getMaxScore(item)"
:precision="2"
:disabled="disabled"
controls-position="right"
size="small"
@change="handleScoreChange"
/>
<span class="deduction-range">({{ getMinScore(item) }}-{{ getMaxScore(item) }}分)</span>
<el-button v-if="!disabled" link type="danger" @click="removeItem(index)">删除</el-button>
</div>
<div v-if="getTypeRemark(item.disciplineTypeId)" class="deduction-remark">
<el-icon class="remark-icon"><InfoFilled /></el-icon>
<span>{{ getTypeRemark(item.disciplineTypeId) }}</span>
</div>
</div>
<div class="deduction-total">
合计扣分:<strong>{{ totalScore }}</strong> 分
<span v-if="totalScore > 100" class="total-warning">(已超过100分上限)</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import type { HygieneDeductionItem } from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types';
interface DisciplineTypeOption {
id: number | string;
typeName: string;
minScore?: number | string;
maxScore?: number | string;
remark?: string;
}
const props = withDefaults(
defineProps<{
modelValue: HygieneDeductionItem[];
options: DisciplineTypeOption[];
disabled?: boolean;
}>(),
{
modelValue: () => [],
options: () => [],
disabled: false
}
);
const emit = defineEmits<{
(e: 'update:modelValue', value: HygieneDeductionItem[]): void;
(e: 'update:totalScore', value: number): void;
}>();
const selectedTypeIds = ref<Array<number | string>>([]);
const items = computed({
get: () => props.modelValue || [],
set: (value: HygieneDeductionItem[]) => emit('update:modelValue', value)
});
const totalScore = computed(() => {
const sum = (props.modelValue || []).reduce((acc, item) => acc + (Number(item.score) || 0), 0);
return Number(sum.toFixed(2));
});
watch(
() => props.modelValue,
(value) => {
selectedTypeIds.value = (value || []).map((item) => item.disciplineTypeId).filter((id) => id !== undefined && id !== null);
emit('update:totalScore', totalScore.value);
},
{ deep: true, immediate: true }
);
const formatOptionLabel = (item: DisciplineTypeOption) => {
const min = item.minScore ?? 0;
return `${item.typeName}(${min}分起)`;
};
const getTypeOption = (typeId?: number | string) => props.options.find((item) => String(item.id) === String(typeId));
const getMinScore = (item: HygieneDeductionItem) => {
const option = getTypeOption(item.disciplineTypeId);
return Number(option?.minScore ?? item.minScore ?? 0);
};
const getMaxScore = (item: HygieneDeductionItem) => {
const option = getTypeOption(item.disciplineTypeId);
return Number(option?.maxScore ?? item.maxScore ?? 100);
};
const getTypeRemark = (typeId?: number | string) => getTypeOption(typeId)?.remark || '';
const emitItems = (nextItems: HygieneDeductionItem[]) => {
items.value = nextItems;
emit('update:totalScore', nextItems.reduce((acc, item) => acc + (Number(item.score) || 0), 0));
};
const handleTypeSelectionChange = (typeIds: Array<number | string>) => {
const currentMap = new Map((props.modelValue || []).map((item) => [String(item.disciplineTypeId), item]));
const nextItems: HygieneDeductionItem[] = [];
typeIds.forEach((typeId, index) => {
const option = getTypeOption(typeId);
if (!option) {
return;
}
const existing = currentMap.get(String(typeId));
nextItems.push({
disciplineTypeId: option.id,
disciplineTypeName: option.typeName,
score: existing?.score ?? Number(option.minScore ?? 0),
minScore: Number(option.minScore ?? 0),
maxScore: Number(option.maxScore ?? 100),
sortOrder: index + 1
});
});
emitItems(nextItems);
if (nextItems.length > typeIds.length - 1) {
ElMessage.success('已添加扣分项');
}
};
const handleScoreChange = () => {
if (totalScore.value > 100) {
ElMessage.warning('合计扣分不能超过100分');
}
emit('update:totalScore', totalScore.value);
};
const removeItem = (index: number) => {
const nextItems = [...(props.modelValue || [])];
nextItems.splice(index, 1);
selectedTypeIds.value = nextItems.map((item) => item.disciplineTypeId);
emitItems(nextItems);
};
</script>
<style scoped>
.deduction-list {
margin-top: 8px;
padding: 12px;
background: #f8f9fb;
border-radius: 8px;
}
.deduction-item + .deduction-item {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #e4e7ed;
}
.deduction-item-main {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.deduction-name {
min-width: 120px;
font-weight: 500;
color: #303133;
}
.deduction-range {
color: #909399;
font-size: 12px;
}
.deduction-remark {
display: flex;
align-items: flex-start;
gap: 6px;
margin-top: 6px;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
.deduction-total {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #e4e7ed;
color: #606266;
}
.deduction-total strong {
color: #f56c6c;
font-size: 16px;
}
.total-warning {
color: #f56c6c;
margin-left: 8px;
}
.empty-text {
color: #909399;
}
</style>
@@ -11,8 +11,8 @@
</div> </div>
</el-form-item> </el-form-item>
<!-- 违纪类型选择 --> <!-- 违纪类型选择:编辑模式单选 -->
<el-form-item label="违纪类型" prop="disciplineTypeId" class="discipline-form-item"> <el-form-item v-if="isEditMode" label="违纪类型" prop="disciplineTypeId" class="discipline-form-item">
<el-select v-model="form.disciplineTypeId" placeholder="请选择违纪类型" style="width: 100%" @change="handleDisciplineTypeChange" filterable> <el-select v-model="form.disciplineTypeId" placeholder="请选择违纪类型" style="width: 100%" @change="handleDisciplineTypeChange" filterable>
<el-option <el-option
v-for="item in allViolationTypes" v-for="item in allViolationTypes"
@@ -31,13 +31,70 @@
</el-option> </el-option>
</el-select> </el-select>
<!-- 违纪类型说明展示 -->
<div v-if="form.disciplineTypeId && selectedDisciplineTypeRemark" class="discipline-type-remark"> <div v-if="form.disciplineTypeId && selectedDisciplineTypeRemark" class="discipline-type-remark">
<el-icon class="remark-icon"><InfoFilled /></el-icon> <el-icon class="remark-icon"><InfoFilled /></el-icon>
<span class="remark-text">类型说明:{{ selectedDisciplineTypeRemark }}</span> <span class="remark-text">类型说明:{{ selectedDisciplineTypeRemark }}</span>
</div> </div>
</el-form-item> </el-form-item>
<!-- 违纪类型选择:新增模式多选 -->
<template v-else>
<el-form-item label="违纪类型" class="discipline-form-item">
<el-select
v-model="selectedTypeIds"
multiple
collapse-tags
collapse-tags-tooltip
filterable
clearable
placeholder="请选择违纪类型(可多选)"
style="width: 100%"
@change="handleTypeSelectionChange"
>
<el-option
v-for="item in allViolationTypes"
:key="item.id"
:label="getViolationTypeLabel(item)"
:value="item.id"
:disabled="item.status !== '0'"
>
<div class="violation-type-option">
<span class="violation-type-name" :title="getViolationTypeLabel(item)">
<span class="area-tag" :class="getAreaTypeClass(item.areaType)">{{ getAreaTypeText(item.areaType) }}</span>
{{ getShortTypeName(item.typeName) }}
</span>
<span class="score-range">{{ item.minScore }}-{{ item.maxScore }}分</span>
</div>
</el-option>
</el-select>
</el-form-item>
<div v-if="violationItems.length > 0" class="violation-item-list">
<div v-for="(item, index) in violationItems" :key="item.disciplineTypeId" class="violation-item-row">
<div class="violation-item-main">
<span class="violation-item-name">{{ item.disciplineTypeName }}</span>
<el-input-number
v-model="item.scoreDeduction"
:min="getItemMinScore(item)"
:max="getItemMaxScore(item)"
:precision="2"
controls-position="right"
size="small"
/>
<span class="score-range">({{ getItemMinScore(item) }}-{{ getItemMaxScore(item) }}分)</span>
<el-button link type="danger" @click="removeViolationItem(index)">删除</el-button>
</div>
<div v-if="getItemRemark(item.disciplineTypeId)" class="discipline-type-remark inline-remark">
<el-icon class="remark-icon"><InfoFilled /></el-icon>
<span class="remark-text">类型说明:{{ getItemRemark(item.disciplineTypeId) }}</span>
</div>
</div>
<div class="violation-item-total">
合计扣分:<strong>{{ violationItemsTotalScore }}</strong> 分
</div>
</div>
</template>
<!-- 违纪日期和扣分分值 - 同一行显示 --> <!-- 违纪日期和扣分分值 - 同一行显示 -->
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
@@ -53,12 +110,8 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="扣分分值" prop="scoreDeduction"> <el-form-item v-if="isEditMode" label="扣分分值" prop="scoreDeduction">
<el-input-number v-model="form.scoreDeduction" :precision="2" placeholder="请输入扣分" style="width: 100%" /> <el-input-number v-model="form.scoreDeduction" :precision="2" placeholder="请输入扣分" style="width: 100%" />
<!-- 违纪类型参考范围提示 -->
<!-- <div v-if="form.disciplineTypeId" class="score-range-reference" style="font-size: 12px; color: #999; margin-top: 4px">
参考范围:{{ currentTypeMinScore }} - {{ currentTypeMaxScore }}分
</div> -->
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -186,7 +239,7 @@ interface Props {
// 组件事件 // 组件事件
interface Emits { interface Emits {
(e: 'update:modelValue', value: boolean): void; (e: 'update:modelValue', value: boolean): void;
(e: 'confirm', data: any): void; (e: 'confirm', data: any | any[]): void;
(e: 'close'): void; (e: 'close'): void;
} }
@@ -207,6 +260,15 @@ const attachmentList = ref<any[]>([]);
// 所有违纪类型列表 // 所有违纪类型列表
const allViolationTypes = ref<any[]>([]); const allViolationTypes = ref<any[]>([]);
interface ViolationItem {
disciplineTypeId: string | number;
disciplineTypeName: string;
scoreDeduction: number;
}
const selectedTypeIds = ref<Array<string | number>>([]);
const violationItems = ref<ViolationItem[]>([]);
// 当前选择的违纪类型扣分范围 // 当前选择的违纪类型扣分范围
const currentTypeMinScore = ref(0); const currentTypeMinScore = ref(0);
const currentTypeMaxScore = ref(100); const currentTypeMaxScore = ref(100);
@@ -236,8 +298,7 @@ const form = reactive({
const rules = reactive({ const rules = reactive({
disciplineTypeId: [{ required: true, message: '请选择违纪类型', trigger: 'change' }], disciplineTypeId: [{ required: true, message: '请选择违纪类型', trigger: 'change' }],
disciplineDate: [{ required: true, message: '请选择违纪日期', trigger: 'change' }], disciplineDate: [{ required: true, message: '请选择违纪日期', trigger: 'change' }],
scoreDeduction: [{ required: true, message: '请输入扣分分值', trigger: 'blur' }], scoreDeduction: [{ required: true, message: '请输入扣分分值', trigger: 'blur' }]
disciplineReason: [{ required: true, message: '请输入违纪原因', trigger: 'blur' }]
}); });
// 计算属性 // 计算属性
@@ -248,6 +309,13 @@ const dialogVisible = computed({
const dialogTitle = computed(() => props.title); 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(() => { const uploadAction = computed(() => {
return import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'; 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<string | number>) => {
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 handleDisciplineTypeChange = (typeId: string | number) => {
const selectedType = allViolationTypes.value.find((item) => item.id === typeId); const selectedType = allViolationTypes.value.find((item) => item.id === typeId);
@@ -414,8 +543,9 @@ const initForm = () => {
} }
} else { } else {
console.log('违纪记录:新增模式,重置附件列表'); console.log('违纪记录:新增模式,重置附件列表');
// 重置附件列表
attachmentList.value = []; attachmentList.value = [];
selectedTypeIds.value = [];
violationItems.value = [];
} }
}; };
@@ -695,15 +825,49 @@ const handleDownloadAttachment = async (file: any) => {
// 确认提交 // 确认提交
const handleConfirm = async () => { const handleConfirm = async () => {
try { try {
await violationFormRef.value?.validate();
buttonLoading.value = true; buttonLoading.value = true;
// 处理附件数据 if (isEditMode.value) {
const attachmentOssIds = attachmentList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId); await violationFormRef.value?.validate();
form.attachmentOssIds = attachmentOssIds.join(','); 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) { } catch (error) {
console.error('提交失败:', error); console.error('提交失败:', error);
ElMessage.error('请检查表单填写是否完整'); ElMessage.error('请检查表单填写是否完整');
@@ -729,6 +893,8 @@ const handleClose = () => {
attachmentList.value = []; attachmentList.value = [];
currentTypeMinScore.value = 0; currentTypeMinScore.value = 0;
currentTypeMaxScore.value = 100; currentTypeMaxScore.value = 100;
selectedTypeIds.value = [];
violationItems.value = [];
}; };
// 重置表单 // 重置表单
@@ -775,6 +941,8 @@ const resetForm = () => {
attachmentList.value = []; attachmentList.value = [];
currentTypeMinScore.value = 0; currentTypeMinScore.value = 0;
currentTypeMaxScore.value = 100; currentTypeMaxScore.value = 100;
selectedTypeIds.value = [];
violationItems.value = [];
}; };
// 获取违纪类型说明 // 获取违纪类型说明
@@ -898,6 +1066,46 @@ onMounted(() => {
line-height: 1.4; 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 { .area-tag {
display: inline-block; display: inline-block;
@@ -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<T extends { studentId?: unknown; studentNo?: string; gender?: unknown }>(
students: T[],
roomStudents: Array<{ studentId?: unknown; studentNo?: string; gender?: unknown }>
): T[] {
if (!students.length || !roomStudents.length) {
return students;
}
const genderByStudentId = new Map<string, string>();
const genderByStudentNo = new Map<string, string>();
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;
});
}
@@ -107,6 +107,7 @@
<el-table-column label="房间名称" align="center" prop="roomName" /> <el-table-column label="房间名称" align="center" prop="roomName" />
<el-table-column label="宿舍楼名称" align="center" prop="buildingName" /> <el-table-column label="宿舍楼名称" align="center" prop="buildingName" />
<el-table-column label="校区名称" align="center" prop="campusName" /> <el-table-column label="校区名称" align="center" prop="campusName" />
<el-table-column label="扣分项" align="center" prop="disciplineTypeName" min-width="160" show-overflow-tooltip />
<el-table-column label="宿舍扣分" align="center" prop="totalScore"> <el-table-column label="宿舍扣分" align="center" prop="totalScore">
<template #default="scope"> <template #default="scope">
<span>-{{ scope.row.totalScore }}</span> <span>-{{ scope.row.totalScore }}</span>
@@ -163,6 +164,7 @@
<!-- 编辑宿舍卫生检查弹窗组件(修改使用) --> <!-- 编辑宿舍卫生检查弹窗组件(修改使用) -->
<DormHygieneCheckEditDialog <DormHygieneCheckEditDialog
:key="editDialogKey"
:visible="editDialog.visible" :visible="editDialog.visible"
:title="editDialog.title" :title="editDialog.title"
:form-data="currentEditData" :form-data="currentEditData"
@@ -186,7 +188,8 @@ import {
delDormHygieneCheck, delDormHygieneCheck,
addDormHygieneCheck, addDormHygieneCheck,
updateDormHygieneCheck, updateDormHygieneCheck,
exportDormHygieneCheck exportDormHygieneCheck,
getViolationsByCheckId
} from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index'; } from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index';
import { import {
DormHygieneCheckVO, DormHygieneCheckVO,
@@ -196,6 +199,7 @@ import {
} from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types'; } from '@/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/types';
import DormHygieneCheckMainDialog from './DormHygieneCheckMainDialog.vue'; import DormHygieneCheckMainDialog from './DormHygieneCheckMainDialog.vue';
import DormHygieneCheckEditDialog from './DormHygieneCheckEditDialog.vue'; import DormHygieneCheckEditDialog from './DormHygieneCheckEditDialog.vue';
import { normalizeId } from './studentIdUtils';
// 类型定义 // 类型定义
interface DialogOption { interface DialogOption {
@@ -238,6 +242,7 @@ const editDialog = reactive<DialogOption>({
// 存储当前编辑的数据 // 存储当前编辑的数据
const currentEditData = ref<any>(null); const currentEditData = ref<any>(null);
const editDialogKey = ref(0);
const initFormData: DormHygieneCheckForm = { const initFormData: DormHygieneCheckForm = {
id: undefined, id: undefined,
@@ -311,7 +316,10 @@ const getList = async () => {
loading.value = true; loading.value = true;
try { try {
const res = await listDormHygieneCheck(queryParams.value); const res = await listDormHygieneCheck(queryParams.value);
dormHygieneCheckList.value = res.rows; dormHygieneCheckList.value = (res.rows || []).map((row: DormHygieneCheckVO) => ({
...row,
id: row.id != null ? normalizeId(row.id) : row.id
}));
total.value = res.total; total.value = res.total;
} catch (error) { } catch (error) {
console.error('查询列表失败:', error); console.error('查询列表失败:', error);
@@ -407,7 +415,6 @@ const handleAdd = () => {
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = async (row?: DormHygieneCheckVO) => { const handleUpdate = async (row?: DormHygieneCheckVO) => {
editDialog.visible = true;
editDialog.title = '修改宿舍卫生检查'; editDialog.title = '修改宿舍卫生检查';
const loadingInstance = ElLoading.service({ const loadingInstance = ElLoading.service({
@@ -417,9 +424,15 @@ const handleUpdate = async (row?: DormHygieneCheckVO) => {
}); });
try { try {
const _id = row?.id || ids.value[0]; const _id = normalizeId(row?.id || ids.value[0]);
const res = await getDormHygieneCheck(_id); const [res, violationsRes] = await Promise.all([getDormHygieneCheck(_id), getViolationsByCheckId(_id)]);
currentEditData.value = res.data; currentEditData.value = {
...res.data,
id: res.data?.id != null ? normalizeId(res.data.id) : res.data?.id,
_prefetchedViolations: violationsRes.data || []
};
editDialogKey.value += 1;
editDialog.visible = true;
} catch (error) { } catch (error) {
console.error('加载编辑数据失败:', error); console.error('加载编辑数据失败:', error);
ElMessage.error('加载数据失败,请重试'); ElMessage.error('加载数据失败,请重试');
@@ -0,0 +1,10 @@
/** 雪花ID统一转字符串,避免 JS Number 精度丢失导致 Map/接口匹配失败 */
export function normalizeStudentId(studentId: unknown): string {
if (studentId === null || studentId === undefined || studentId === '') {
return '';
}
return String(studentId);
}
/** 通用雪花ID归一化(检查ID、学生ID等) */
export const normalizeId = normalizeStudentId;
@@ -0,0 +1,7 @@
/** 计算学生有效违纪记录的扣分合计(已撤销 status=2 不计入) */
export function calcStudentViolationScoreTotal(violations: any[] | undefined | null): number {
const total = (violations || [])
.filter((item) => item?.status !== '2')
.reduce((sum, item) => sum + (parseFloat(String(item?.scoreDeduction ?? 0)) || 0), 0);
return parseFloat(total.toFixed(2));
}