wuzhen 更新假期住宿申请

This commit is contained in:
“wuzhen” committed 2026-08-20 15:17:40 +08:00
1 parent 1d67506451
commit 3518ea697e
35 files changed
+2173 -98

No files matched your search

@@ -5,7 +5,6 @@ import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
@@ -19,6 +18,7 @@ import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgHolidayStayVo;
import org.dromara.xg.domain.bo.XgHolidayStayBo;
import org.dromara.xg.domain.bo.XgHolidayStayAuditBo;
import org.dromara.xg.domain.vo.StudentOptionVo;
import org.dromara.xg.domain.bo.StudentOptionBo;
import org.dromara.xg.service.IXgHolidayStayService;
@@ -48,7 +48,7 @@ public class XgHolidayStayController extends BaseController {
}
/**
* 导出假期留宿管理列表
* 导出假期留宿管理列表(可按 applyStatus 筛选)
*/
// @SaCheckPermission("holidayStay:holidayStay:export")
@Log(title = "假期留宿管理", businessType = BusinessType.EXPORT)
@@ -58,10 +58,88 @@ public class XgHolidayStayController extends BaseController {
ExcelUtil.exportExcel(list, "假期留宿管理", XgHolidayStayVo.class, response);
}
/**
* 获取学生选择器选项列表(支持按学号、姓名检索)
*/
// @SaCheckPermission("holidayStay:holidayStay:list")
@GetMapping("/studentOptions")
public TableDataInfo<StudentOptionVo> getStudentOptions(StudentOptionBo bo, PageQuery pageQuery) {
return xgHolidayStayService.getStudentOptions(bo, pageQuery);
}
/**
* 查询单人留宿有效数据
*/
// @SaCheckPermission("holidayStay:holidayStay:list")
@GetMapping("/singlePersonValidStay")
public R<List<XgHolidayStayVo>> getSinglePersonValidStay() {
return R.ok(xgHolidayStayService.querySinglePersonValidStay());
}
/**
* 学生本人提交留宿申请
*/
// @SaCheckPermission("holidayStay:holidayStay:apply")
@Log(title = "假期留宿申请", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/apply")
public R<Void> apply(@RequestBody XgHolidayStayBo bo) {
return toAjax(xgHolidayStayService.studentApply(bo));
}
/**
* 当前学生的申请列表
*/
// @SaCheckPermission("holidayStay:holidayStay:apply")
@GetMapping("/myList")
public TableDataInfo<XgHolidayStayVo> myList(XgHolidayStayBo bo, PageQuery pageQuery) {
return xgHolidayStayService.queryMyApplyPage(bo, pageQuery);
}
/**
* 班主任本班申请列表
*/
// @SaCheckPermission("holidayStay:holidayStay:list1")
@GetMapping("/teacherList")
public TableDataInfo<XgHolidayStayVo> teacherList(XgHolidayStayBo bo, PageQuery pageQuery) {
return xgHolidayStayService.queryTeacherPage(bo, pageQuery);
}
/**
* 班主任审批通过
*/
// @SaCheckPermission("holidayStay:holidayStay:audit")
@Log(title = "假期留宿审批通过", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/approve")
public R<Void> approve(@Validated @RequestBody XgHolidayStayAuditBo auditBo) {
return toAjax(xgHolidayStayService.approve(auditBo));
}
/**
* 班主任驳回
*/
// @SaCheckPermission("holidayStay:holidayStay:audit")
@Log(title = "假期留宿驳回", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/reject")
public R<Void> reject(@Validated @RequestBody XgHolidayStayAuditBo auditBo) {
return toAjax(xgHolidayStayService.reject(auditBo));
}
/**
* 班主任撤销已通过申请
*/
// @SaCheckPermission("holidayStay:holidayStay:revoke")
@Log(title = "假期留宿撤销", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/revoke")
public R<Void> revoke(@Validated(XgHolidayStayAuditBo.Revoke.class) @RequestBody XgHolidayStayAuditBo auditBo) {
return toAjax(xgHolidayStayService.revoke(auditBo));
}
/**
* 获取假期留宿管理详细信息
*
* @param id 主键
*/
// @SaCheckPermission("holidayStay:holidayStay:query")
@GetMapping("/{id}")
@@ -70,7 +148,7 @@ public class XgHolidayStayController extends BaseController {
}
/**
* 新增假期留宿管理
* 新增假期留宿管理(宿管登记,默认已通过)
*/
// @SaCheckPermission("holidayStay:holidayStay:add")
@Log(title = "假期留宿管理", businessType = BusinessType.INSERT)
@@ -93,8 +171,6 @@ public class XgHolidayStayController extends BaseController {
/**
* 删除假期留宿管理
*
* @param ids 主键串
*/
// @SaCheckPermission("holidayStay:holidayStay:remove")
@Log(title = "假期留宿管理", businessType = BusinessType.DELETE)
@@ -102,24 +178,4 @@ public class XgHolidayStayController extends BaseController {
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable("ids") Long[] ids) {
return toAjax(xgHolidayStayService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 获取学生选择器选项列表(支持按学号、姓名检索)
*/
// @SaCheckPermission("holidayStay:holidayStay:list")
@GetMapping("/studentOptions")
public TableDataInfo<StudentOptionVo> getStudentOptions(StudentOptionBo bo, PageQuery pageQuery) {
return xgHolidayStayService.getStudentOptions(bo, pageQuery);
}
/**
* 查询单人留宿有效数据
* 获取所有有效的假期留宿数据,筛选出在同一时间段内该宿舍只有一个学生留宿的记录
*/
// @SaCheckPermission("holidayStay:holidayStay:list")
@GetMapping("/singlePersonValidStay")
public R<List<XgHolidayStayVo>> getSinglePersonValidStay() {
List<XgHolidayStayVo> result = xgHolidayStayService.querySinglePersonValidStay();
return R.ok(result);
}
}
@@ -110,6 +110,16 @@ public class XgDormHygieneCheck extends TenantEntity {
*/
private String campusName;
/**
* 归属班级ID(宿舍扣分归属,取自当前寝室在住学生班级)
*/
private Long classId;
/**
* 归属班级名称
*/
private String className;
/**
* 宿舍扣分(0-100分)
*/
@@ -199,5 +199,55 @@ public class XgHolidayStay extends TenantEntity {
*/
private String remark;
/**
* 申请状态(0草稿 1待审 2已通过 3已驳回 4已撤销)
*/
private String applyStatus;
/**
* 提交申请时间
*/
private Date applyTime;
/**
* 审批人用户ID
*/
private Long auditorId;
/**
* 审批人姓名
*/
private String auditorName;
/**
* 审批时间
*/
private Date auditTime;
/**
* 审批意见
*/
private String auditOpinion;
/**
* 撤销人用户ID
*/
private Long revokeBy;
/**
* 撤销人姓名
*/
private String revokeByName;
/**
* 撤销时间
*/
private Date revokeTime;
/**
* 撤销原因
*/
private String revokeReason;
}
@@ -117,6 +117,18 @@ public class XgDormHygieneCheckBo extends BaseEntity {
*/
private String campusName;
/**
* 归属班级ID(宿舍扣分归属,取自当前寝室在住学生班级)
*/
@NotNull(message = "归属班级不能为空", groups = { AddGroup.class, EditGroup.class })
private Long classId;
/**
* 归属班级名称
*/
@NotBlank(message = "归属班级名称不能为空", groups = { AddGroup.class, EditGroup.class })
private String className;
/**
* 宿舍总分(0-100分)
*/
@@ -118,6 +118,18 @@ public class XgDormHygieneCheckCompleteBo {
*/
private String campusName;
/**
* 归属班级ID(宿舍扣分归属,取自当前寝室在住学生班级)
*/
@NotNull(message = "归属班级不能为空", groups = { AddGroup.class, EditGroup.class })
private Long classId;
/**
* 归属班级名称
*/
@NotBlank(message = "归属班级名称不能为空", groups = { AddGroup.class, EditGroup.class })
private String className;
/**
* 宿舍总分(0-100分)
*/
@@ -0,0 +1,35 @@
package org.dromara.xg.domain.bo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 假期留宿审批/撤销参数
*/
@Data
public class XgHolidayStayAuditBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@NotNull(message = "申请ID不能为空")
private Long id;
/**
* 审批意见(驳回建议填写)
*/
private String auditOpinion;
/**
* 撤销原因(撤销必填)
*/
@NotBlank(message = "撤销原因不能为空", groups = { Revoke.class })
private String revokeReason;
public interface Revoke {
}
}
@@ -209,5 +209,23 @@ public class XgHolidayStayBo extends BaseEntity {
*/
private String remark;
/**
* 申请状态(0草稿 1待审 2已通过 3已驳回 4已撤销)
*/
private String applyStatus;
/**
* 提交申请时间
*/
private Date applyTime;
private Long auditorId;
private String auditorName;
private Date auditTime;
private String auditOpinion;
private Long revokeBy;
private String revokeByName;
private Date revokeTime;
private String revokeReason;
}
@@ -123,6 +123,17 @@ public class XgDormHygieneCheckSimplifiedVo implements Serializable {
@ExcelProperty(value = "校区名称")
private String campusName;
/**
* 归属班级ID(宿舍扣分归属)
*/
private Long classId;
/**
* 归属班级名称
*/
@ExcelProperty(value = "归属班级")
private String className;
/**
* 宿舍总分(0-100分)
*/
@@ -135,6 +135,18 @@ public class XgDormHygieneCheckVo implements Serializable {
@ExcelProperty(value = "校区名称")
private String campusName;
/**
* 归属班级ID(宿舍扣分归属)
*/
@ExcelProperty(value = "归属班级ID")
private Long classId;
/**
* 归属班级名称
*/
@ExcelProperty(value = "归属班级")
private String className;
/**
* 宿舍总分(0-100分)
*/
@@ -236,4 +236,39 @@ public class XgHolidayStayVo implements Serializable {
@ExcelProperty(value = "备注信息")
private String remark;
/**
* 申请状态(0草稿 1待审 2已通过 3已驳回 4已撤销)
*/
@ExcelProperty(value = "申请状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "xg_holiday_stay_apply_status")
private String applyStatus;
@ExcelProperty(value = "提交申请时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date applyTime;
private Long auditorId;
@ExcelProperty(value = "审批人")
private String auditorName;
@ExcelProperty(value = "审批时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date auditTime;
@ExcelProperty(value = "审批意见")
private String auditOpinion;
private Long revokeBy;
@ExcelProperty(value = "撤销人")
private String revokeByName;
@ExcelProperty(value = "撤销时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date revokeTime;
@ExcelProperty(value = "撤销原因")
private String revokeReason;
}
@@ -165,14 +165,17 @@ public interface XgDormBedStudentMapper extends BaseMapperPlus<XgDormBedStudent,
b.bed_name,
b.bed_code,
-- 班级信息
c.class_name,
-- 班级信息(优先班级表,回退学生表冗余字段)
COALESCE(c.id, s.class_id) as class_id,
COALESCE(c.class_name, s.class_name) as class_name,
-- 学院信息
d.dept_name as college_name,
COALESCE(c.college_id, s.college_id) as college_id,
COALESCE(d.dept_name, s.college_name) as college_name,
-- 专业信息(通过班级关联)
major.dept_name as major_name
COALESCE(c.major_id, s.major_id) as major_id,
COALESCE(major.dept_name, s.major_name) as major_name
FROM xg_dorm_bed_student bs
INNER JOIN xg_dorm_bed b ON bs.bed_id = b.id AND b.deleted = '0'
@@ -2,6 +2,7 @@ package org.dromara.xg.service;
import org.dromara.xg.domain.vo.XgHolidayStayVo;
import org.dromara.xg.domain.bo.XgHolidayStayBo;
import org.dromara.xg.domain.bo.XgHolidayStayAuditBo;
import org.dromara.xg.domain.vo.StudentOptionVo;
import org.dromara.xg.domain.bo.StudentOptionBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -85,4 +86,34 @@ public interface IXgHolidayStayService {
*/
List<XgHolidayStayVo> querySinglePersonValidStay();
/**
* 学生本人提交留宿申请(状态=待审)
*/
Boolean studentApply(XgHolidayStayBo bo);
/**
* 当前学生的申请列表
*/
TableDataInfo<XgHolidayStayVo> queryMyApplyPage(XgHolidayStayBo bo, PageQuery pageQuery);
/**
* 班主任本班申请列表(含待审)
*/
TableDataInfo<XgHolidayStayVo> queryTeacherPage(XgHolidayStayBo bo, PageQuery pageQuery);
/**
* 班主任审批通过
*/
Boolean approve(XgHolidayStayAuditBo auditBo);
/**
* 班主任驳回
*/
Boolean reject(XgHolidayStayAuditBo auditBo);
/**
* 班主任撤销已通过申请
*/
Boolean revoke(XgHolidayStayAuditBo auditBo);
}
@@ -2500,9 +2500,12 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
studentInfo.put("studentNo", detailMap.get("student_no"));
studentInfo.put("gender", detailMap.get("gender"));
// 学院、专业、班级信息
// 学院、专业、班级信息(含 ID,供卫生检查归属班级等使用)
studentInfo.put("collegeId", detailMap.get("college_id"));
studentInfo.put("collegeName", detailMap.get("college_name"));
studentInfo.put("majorId", detailMap.get("major_id"));
studentInfo.put("majorName", detailMap.get("major_name"));
studentInfo.put("classId", detailMap.get("class_id"));
studentInfo.put("className", detailMap.get("class_name"));
// 床位信息
@@ -2561,33 +2564,35 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
return null;
}
// 组装返回结果
// 组装返回结果(兼容 Map 键为下划线或驼峰)
Map<String, Object> result = new LinkedHashMap<>();
// 校区信息
result.put("campusId", dormInfo.get("campus_id"));
result.put("campusName", dormInfo.get("campus_name"));
result.put("campusId", mapVal(dormInfo, "campusId", "campus_id"));
result.put("campusName", mapVal(dormInfo, "campusName", "campus_name"));
// 宿舍区信息
result.put("areaId", dormInfo.get("area_id"));
result.put("areaName", dormInfo.get("area_name"));
result.put("areaId", mapVal(dormInfo, "areaId", "area_id"));
result.put("areaName", mapVal(dormInfo, "areaName", "area_name"));
// 楼栋信息
result.put("buildingId", dormInfo.get("building_id"));
result.put("buildingName", dormInfo.get("building_name"));
result.put("buildingCode", dormInfo.get("building_code"));
result.put("buildingId", mapVal(dormInfo, "buildingId", "building_id"));
result.put("buildingName", mapVal(dormInfo, "buildingName", "building_name"));
result.put("buildingCode", mapVal(dormInfo, "buildingCode", "building_code"));
// 房间信息
result.put("roomId", dormInfo.get("room_id"));
result.put("roomName", dormInfo.get("room_name"));
result.put("roomNo", dormInfo.get("room_no"));
result.put("roomNumber", dormInfo.get("room_no")); // 兼容字段
result.put("roomId", mapVal(dormInfo, "roomId", "room_id"));
result.put("roomName", mapVal(dormInfo, "roomName", "room_name"));
Object roomNo = mapVal(dormInfo, "roomNo", "room_no");
result.put("roomNo", roomNo);
result.put("roomNumber", roomNo); // 兼容字段
// 床位信息
result.put("bedId", dormInfo.get("bed_id"));
result.put("bedName", dormInfo.get("bed_name"));
result.put("bedNo", dormInfo.get("bed_no"));
result.put("bedNumber", dormInfo.get("bed_no")); // 兼容字段
result.put("bedId", mapVal(dormInfo, "bedId", "bed_id"));
result.put("bedName", mapVal(dormInfo, "bedName", "bed_name"));
Object bedNo = mapVal(dormInfo, "bedNo", "bed_no");
result.put("bedNo", bedNo);
result.put("bedNumber", bedNo); // 兼容字段
log.info("获取学生当前宿舍信息成功,学生ID: {}, 宿舍信息: {}校区-{}楼-{}房间-{}床位",
studentId,
@@ -2604,6 +2609,17 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
}
}
private static Object mapVal(Map<String, Object> map, String camelKey, String snakeKey) {
if (map == null) {
return null;
}
Object val = map.get(camelKey);
if (val == null) {
val = map.get(snakeKey);
}
return val;
}
/**
* 根据床位ID获取该床位的学生信息
* 包含学生基本信息、学院、专业、班级等信息
@@ -11,12 +11,21 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.dromara.xg.domain.bo.XgHolidayStayBo;
import org.dromara.xg.domain.bo.XgHolidayStayAuditBo;
import org.dromara.xg.domain.vo.XgHolidayStayVo;
import org.dromara.xg.domain.XgHolidayStay;
import org.dromara.xg.domain.bo.StudentOptionBo;
import org.dromara.xg.domain.vo.StudentOptionVo;
import org.dromara.xg.domain.vo.XgStudentMsgVo;
import org.dromara.xg.domain.vo.XgClassMsgVo;
import org.dromara.xg.domain.bo.XgClassMsgBo;
import org.dromara.xg.mapper.XgHolidayStayMapper;
import org.dromara.xg.service.IXgHolidayStayService;
import org.dromara.xg.service.IXgStudentMsgService;
import org.dromara.xg.service.IXgClassMsgService;
import org.dromara.xg.service.IXgDormBedStudentService;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.satoken.utils.LoginHelper;
import java.util.List;
import java.util.Map;
@@ -44,6 +53,9 @@ import org.springframework.beans.BeanUtils;
public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
private final XgHolidayStayMapper baseMapper;
private final IXgStudentMsgService xgStudentMsgService;
private final IXgClassMsgService xgClassMsgService;
private final IXgDormBedStudentService xgDormBedStudentService;
/**
* 查询假期留宿管理
@@ -154,6 +166,9 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
// 添加创建人过滤条件 - 实现班主任权限控制,只能查看自己创建的数据
lqw.eq(bo.getCreateBy() != null, XgHolidayStay::getCreateBy, bo.getCreateBy());
// 申请审批状态
lqw.eq(StringUtils.isNotBlank(bo.getApplyStatus()), XgHolidayStay::getApplyStatus, bo.getApplyStatus());
// 添加状态过滤逻辑
addStatusFilter(lqw, bo);
@@ -201,6 +216,10 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
@Override
public Boolean insertByBo(XgHolidayStayBo bo) {
XgHolidayStay add = MapstructUtils.convert(bo, XgHolidayStay.class);
// 宿管/班主任直接登记:默认已通过;学生申请走 studentApply
if (StringUtils.isBlank(add.getApplyStatus())) {
add.setApplyStatus("2");
}
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
@@ -755,4 +774,209 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
return originalStay;
}
}
@Override
public Boolean studentApply(XgHolidayStayBo bo) {
Long userId = LoginHelper.getUserId();
XgStudentMsgVo student = xgStudentMsgService.queryByUserId(userId);
if (student == null || student.getId() == null) {
throw new ServiceException("未找到当前登录学生信息,无法申请留宿");
}
fillStudentSnapshot(bo, student);
fillDormSnapshot(bo, student.getId());
if (bo.getBedId() == null || bo.getRoomId() == null) {
throw new ServiceException("未查询到当前在住宿舍/床位信息,请先完成床位分配后再申请留宿");
}
bo.setApplyStatus("1");
bo.setApplyTime(new Date());
bo.setAuditorId(null);
bo.setAuditorName(null);
bo.setAuditTime(null);
bo.setAuditOpinion(null);
bo.setRevokeBy(null);
bo.setRevokeByName(null);
bo.setRevokeTime(null);
bo.setRevokeReason(null);
return insertByBo(bo);
}
@Override
public TableDataInfo<XgHolidayStayVo> queryMyApplyPage(XgHolidayStayBo bo, PageQuery pageQuery) {
Long userId = LoginHelper.getUserId();
XgStudentMsgVo student = xgStudentMsgService.queryByUserId(userId);
if (student == null || student.getId() == null) {
return TableDataInfo.build(new Page<>());
}
if (bo == null) {
bo = new XgHolidayStayBo();
}
bo.setStudentId(student.getId());
return queryPageList(bo, pageQuery);
}
@Override
public TableDataInfo<XgHolidayStayVo> queryTeacherPage(XgHolidayStayBo bo, PageQuery pageQuery) {
List<Long> classIds = listMyClassIdsAsTeacher();
if (classIds.isEmpty()) {
return TableDataInfo.build(new Page<>());
}
if (bo == null) {
bo = new XgHolidayStayBo();
}
// 指定班级时必须是本班
if (bo.getClassId() != null && !classIds.contains(bo.getClassId())) {
return TableDataInfo.build(new Page<>());
}
LambdaQueryWrapper<XgHolidayStay> lqw = buildQueryWrapper(bo);
if (bo.getClassId() == null) {
lqw.in(XgHolidayStay::getClassId, classIds);
}
lqw.orderByDesc(XgHolidayStay::getApplyTime).orderByDesc(XgHolidayStay::getId);
Page<XgHolidayStayVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
@Override
public Boolean approve(XgHolidayStayAuditBo auditBo) {
XgHolidayStay entity = requireTeacherPendingRecord(auditBo.getId());
entity.setApplyStatus("2");
entity.setAuditorId(LoginHelper.getUserId());
entity.setAuditorName(LoginHelper.getNickname());
entity.setAuditTime(new Date());
entity.setAuditOpinion(auditBo.getAuditOpinion());
return baseMapper.updateById(entity) > 0;
}
@Override
public Boolean reject(XgHolidayStayAuditBo auditBo) {
XgHolidayStay entity = requireTeacherPendingRecord(auditBo.getId());
entity.setApplyStatus("3");
entity.setAuditorId(LoginHelper.getUserId());
entity.setAuditorName(LoginHelper.getNickname());
entity.setAuditTime(new Date());
entity.setAuditOpinion(auditBo.getAuditOpinion());
return baseMapper.updateById(entity) > 0;
}
@Override
public Boolean revoke(XgHolidayStayAuditBo auditBo) {
if (StringUtils.isBlank(auditBo.getRevokeReason())) {
throw new ServiceException("撤销原因不能为空");
}
XgHolidayStay entity = requireTeacherOwnedRecord(auditBo.getId());
if (!"2".equals(entity.getApplyStatus())) {
throw new ServiceException("仅已通过的申请可撤销");
}
entity.setApplyStatus("4");
entity.setRevokeBy(LoginHelper.getUserId());
entity.setRevokeByName(LoginHelper.getNickname());
entity.setRevokeTime(new Date());
entity.setRevokeReason(auditBo.getRevokeReason());
return baseMapper.updateById(entity) > 0;
}
private void fillStudentSnapshot(XgHolidayStayBo bo, XgStudentMsgVo student) {
bo.setStudentId(student.getId());
bo.setStudentName(student.getName());
bo.setStudentNo(student.getStudentNo());
if (StringUtils.isNotBlank(student.getClassId())) {
try {
bo.setClassId(Long.valueOf(student.getClassId()));
} catch (NumberFormatException ignored) {
// ignore
}
}
bo.setClassName(student.getClassName());
if (StringUtils.isNotBlank(student.getMajorId())) {
try {
bo.setMajorId(Long.valueOf(student.getMajorId()));
} catch (NumberFormatException ignored) {
// ignore
}
}
bo.setMajorName(student.getMajorName());
if (StringUtils.isNotBlank(student.getCollegeId())) {
try {
bo.setCollegeId(Long.valueOf(student.getCollegeId()));
} catch (NumberFormatException ignored) {
// ignore
}
}
bo.setCollegeName(student.getCollegeName());
if (StringUtils.isBlank(bo.getStudentPhone())) {
bo.setStudentPhone(student.getPhone());
}
}
@SuppressWarnings("unchecked")
private void fillDormSnapshot(XgHolidayStayBo bo, Long studentId) {
Object dormObj = xgDormBedStudentService.getCurrentDormInfo(studentId);
if (!(dormObj instanceof Map)) {
return;
}
Map<String, Object> dorm = (Map<String, Object>) dormObj;
bo.setCampusId(toLong(dorm.get("campusId")));
bo.setCampusName(toStr(dorm.get("campusName")));
bo.setAreaId(toLong(dorm.get("areaId")));
bo.setAreaName(toStr(dorm.get("areaName")));
bo.setBuildingId(toLong(dorm.get("buildingId")));
bo.setBuildingName(toStr(dorm.get("buildingName")));
bo.setRoomId(toLong(dorm.get("roomId")));
bo.setRoomName(toStr(dorm.get("roomName")));
bo.setBedId(toLong(dorm.get("bedId")));
Object bedName = dorm.get("bedName");
if (bedName == null) {
bedName = dorm.get("bedNo");
}
bo.setBedName(toStr(bedName));
}
private List<Long> listMyClassIdsAsTeacher() {
XgClassMsgBo classBo = new XgClassMsgBo();
classBo.setClassTeacherId(LoginHelper.getUserId());
classBo.setClassStatus("1");
List<XgClassMsgVo> classes = xgClassMsgService.queryList(classBo);
if (classes == null || classes.isEmpty()) {
return List.of();
}
return classes.stream().map(XgClassMsgVo::getId).filter(id -> id != null).collect(Collectors.toList());
}
private XgHolidayStay requireTeacherPendingRecord(Long id) {
XgHolidayStay entity = requireTeacherOwnedRecord(id);
if (!"1".equals(entity.getApplyStatus())) {
throw new ServiceException("仅待审申请可审批");
}
return entity;
}
private XgHolidayStay requireTeacherOwnedRecord(Long id) {
XgHolidayStay entity = baseMapper.selectById(id);
if (entity == null || (entity.getDeleted() != null && entity.getDeleted() != 0L)) {
throw new ServiceException("申请记录不存在");
}
List<Long> classIds = listMyClassIdsAsTeacher();
if (entity.getClassId() == null || !classIds.contains(entity.getClassId())) {
throw new ServiceException("无权操作非本班学生的留宿申请");
}
return entity;
}
private Long toLong(Object val) {
if (val == null) {
return null;
}
if (val instanceof Number) {
return ((Number) val).longValue();
}
try {
return Long.valueOf(val.toString());
} catch (Exception e) {
return null;
}
}
private String toStr(Object val) {
return val == null ? null : val.toString();
}
}
@@ -107,6 +107,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="campusName != null and campusName != ''">
AND dhc.campus_name LIKE CONCAT('%', #{campusName}, '%')
</if>
<if test="classId != null">
AND dhc.class_id = #{classId}
</if>
<if test="className != null and className != ''">
AND dhc.class_name LIKE CONCAT('%', #{className}, '%')
</if>
<if test="hygieneGrade != null and hygieneGrade != ''">
AND dhc.hygiene_grade = #{hygieneGrade}
</if>
+1 -1
View File
@@ -20,7 +20,7 @@
- [宿舍管理员授权API](./宿舍管理员授权API.md)
- [学生管理员授权关系API](./学生管理员授权关系API.md)
- [学生销假管理API](./学生销假管理API.md)
- [假期留宿管理API](./假期留宿管理API.md) - **重要修复:单人留宿计算逻辑**(2025-01-03)
- [假期留宿管理API](./假期留宿管理API.md) - **申请/班主任一级审批/撤销/导出**(2026-08-04)
- [均化处理最高分值API](./均化处理最高分值API.md) - **新增**:支持考核申请均化处理功能(2025-01-03)
### 1. 用户认证登录
@@ -34,6 +34,8 @@
- 单项分数必须在对应类型的 min-max 范围内
- 合计扣分上限 100 分
- 历史单条数据迁移:以主表 `total_score` 作为唯一明细的 score
- **归属班级(classId/className)必填**:选项仅来自当前寝室在住学生的班级(去重);默认选中第一项;用于宿舍扣分归属统计
- **班级选项数据来源**:`GET /dormBedStudent/dormBedStudent/getRoomStudents` 须返回 `classId`、`className`(以及 collegeId/majorId);前端按 `classId` 去重生成下拉,缺 `classId` 时会出现「当前寝室无在住学生班级」
## API接口规范
@@ -59,6 +61,8 @@
"areaName": "东区",
"campusId": 1,
"campusName": "主校区",
"classId": 10001,
"className": "计算机应用技术2301班",
"totalScore": 85.5,
"deductionItems": [
{
@@ -315,6 +319,7 @@ public interface DormHygieneStudentScoreMapper extends BaseMapperPlus<DormHygien
- `checkDate`: 必填,日期时间格式
- `checkerId`: 必填,关联用户表
- `roomId`: 必填,关联房间表
- `classId` / `className`: **必填**,宿舍扣分归属班级;选项须为当前寝室在住学生班级
- `totalScore`: 可选,0-100分(由 deductionItems 合计自动计算,只读)
- `deductionItems`: 可选,扣分明细数组;每项 score 默认 minScore,可在类型 min-max 内调整;合计不超过100
- `hygieneGrade`: 可选,A/B/C/D
@@ -4376,6 +4376,8 @@ CREATE TABLE `xg_dorm_hygiene_check` (
`area_name` varchar(100) DEFAULT NULL COMMENT '宿舍区名称',
`campus_id` bigint(20) DEFAULT NULL COMMENT '校区ID,关联xg_dorm_campus.id',
`campus_name` varchar(100) DEFAULT NULL COMMENT '校区名称',
`class_id` bigint(20) DEFAULT NULL COMMENT '归属班级ID(宿舍扣分归属)',
`class_name` varchar(100) DEFAULT NULL COMMENT '归属班级名称',
`total_score` decimal(5,2) DEFAULT NULL COMMENT '宿舍扣分(0-100分)',
`hygiene_grade` char(1) DEFAULT NULL COMMENT '卫生等级(A优秀 B良好 C合格 D不合格)',
`attachment_ids` text COMMENT '附件OSS ID列表,逗号分隔',
@@ -4388,6 +4390,7 @@ CREATE TABLE `xg_dorm_hygiene_check` (
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_check_date` (`check_date`),
KEY `idx_class_id` (`class_id`),
KEY `idx_checker_id` (`checker_id`),
KEY `idx_room_id` (`room_id`),
KEY `idx_building_id` (`building_id`),
+5
View File
@@ -0,0 +1,5 @@
allowBuilds:
'@parcel/watcher': set this to true or false
core-js: set this to true or false
fsevents: set this to true or false
puppeteer: set this to true or false
+79
View File
@@ -0,0 +1,79 @@
import { request } from '@/common/request.js';
/** 学生提交留宿申请 */
export const applyHolidayStay = (data) => {
return request({
url: '/holidayStay/holidayStay/apply',
method: 'POST',
data
});
};
/** 当前学生申请列表 */
export const listMyHolidayStay = (query) => {
return request({
url: '/holidayStay/holidayStay/myList',
method: 'GET',
params: query
});
};
/** 班主任本班申请列表 */
export const listTeacherHolidayStay = (query) => {
return request({
url: '/holidayStay/holidayStay/teacherList',
method: 'GET',
params: query
});
};
/** 审批通过 */
export const approveHolidayStay = (data) => {
return request({
url: '/holidayStay/holidayStay/approve',
method: 'POST',
data
});
};
/** 驳回 */
export const rejectHolidayStay = (data) => {
return request({
url: '/holidayStay/holidayStay/reject',
method: 'POST',
data
});
};
/** 撤销已通过申请 */
export const revokeHolidayStay = (data) => {
return request({
url: '/holidayStay/holidayStay/revoke',
method: 'POST',
data
});
};
/** 详情 */
export const getHolidayStay = (id) => {
return request({
url: '/holidayStay/holidayStay/' + id,
method: 'GET'
});
};
/** 获取学生当前在住宿舍信息 */
export const getCurrentDormInfo = (studentId) => {
return request({
url: '/dormBedStudent/dormBedStudent/currentDormInfo/' + studentId,
method: 'GET'
});
};
/** 当前登录学生信息 */
export const getCurrentStudentInfo = () => {
return request({
url: '/studentMsg/studentMsg/currentUserInfo',
method: 'GET'
});
};
+24
View File
@@ -207,6 +207,30 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/xgss/jqzs/myList",
"style": {
"navigationBarTitleText": "我的留宿申请",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xgss/jqzs/apply",
"style": {
"navigationBarTitleText": "申请假期留宿",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xgss/jqzs/teacherList",
"style": {
"navigationBarTitleText": "留宿审批",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xgss/ssdm/ssdm",
"style": {
+34
View File
@@ -111,6 +111,26 @@
</template>
</ServiceItem>
<ServiceItem
v-if="hasPermi(['holidayStay:holidayStay:apply'])"
title="假期留宿申请"
@click="gotoHolidayStayApply"
>
<template #icon>
<u-icon name="home" size="24" color="#3c9cff" />
</template>
</ServiceItem>
<ServiceItem
v-if="hasPermi(['holidayStay:holidayStay:list1'])"
title="假期留宿审批"
@click="gotoHolidayStayAudit"
>
<template #icon>
<u-icon name="checkmark-circle" size="24" color="#3c9cff" />
</template>
</ServiceItem>
<!-- <ServiceItem
v-if="hasPermi(['student:psychology:list'])"
title="心理健康"
@@ -1183,6 +1203,20 @@ export default {
});
},
// 跳转到假期留宿申请(学生)
gotoHolidayStayApply() {
uni.navigateTo({
url: '/pages/xgss/jqzs/myList',
});
},
// 跳转到假期留宿审批(班主任)
gotoHolidayStayAudit() {
uni.navigateTo({
url: '/pages/xgss/jqzs/teacherList',
});
},
// 跳转到实习管理页面
gotoInternshipManage() {
uni.navigateTo({
+370
View File
@@ -0,0 +1,370 @@
<template>
<view class="page-container">
<u-navbar title="申请假期留宿" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<view class="form-wrap">
<view class="dorm-card">
<view class="dorm-title">当前宿舍(自动带出)</view>
<view v-if="dormLoading" class="dorm-text">加载中...</view>
<view v-else-if="dormText" class="dorm-text">{{ dormText }}</view>
<view v-else class="dorm-text dorm-warn">未查询到在住床位,请先完成宿舍床位分配后再申请</view>
</view>
<u-form :model="form" ref="formRef" label-width="90">
<u-form-item label="假期类型" prop="holidayType" required>
<view @click="openHolidayPicker" style="width: 100%">
<u-input
v-model="holidayTypeLabel"
placeholder="请选择假期类型"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
</u-form-item>
<u-form-item label="开始时间" prop="applyStartDate" required>
<view @click="openDate('start')" style="width: 100%">
<u-input
v-model="form.applyStartDate"
placeholder="请选择开始时间"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
</u-form-item>
<u-form-item label="结束时间" prop="applyEndDate" required>
<view @click="openDate('end')" style="width: 100%">
<u-input
v-model="form.applyEndDate"
placeholder="请选择结束时间"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
</u-form-item>
<u-form-item label="原因类型" prop="stayReasonType">
<view @click="openReasonPicker" style="width: 100%">
<u-input
v-model="stayReasonTypeLabel"
placeholder="请选择原因类型"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
</u-form-item>
<u-form-item label="留宿原因" prop="stayReason" required>
<u-textarea
v-model="form.stayReason"
placeholder="请填写留宿原因"
maxlength="500"
count
></u-textarea>
</u-form-item>
<u-form-item label="联系电话" prop="studentPhone">
<u-input v-model="form.studentPhone" placeholder="可自动带出,可修改" />
</u-form-item>
<u-form-item label="紧急联系人" prop="emergencyContact">
<u-input v-model="form.emergencyContact" placeholder="请输入紧急联系人" />
</u-form-item>
<u-form-item label="紧急电话" prop="emergencyPhone">
<u-input v-model="form.emergencyPhone" placeholder="请输入紧急联系电话" />
</u-form-item>
<u-form-item label="家长同意" prop="parentConsent">
<u-radio-group v-model="form.parentConsent" class="radio-group">
<u-radio label="1" name="1" class="radio-item">是</u-radio>
<u-radio label="0" name="0" class="radio-item">否</u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="备注" prop="remark">
<u-textarea v-model="form.remark" placeholder="选填" maxlength="200"></u-textarea>
</u-form-item>
</u-form>
<u-button type="primary" :loading="submitting" text="提交申请" @click="submit"></u-button>
<view class="tip">提交后由班主任一级审批;宿舍信息将按当前入住床位自动带出。</view>
</view>
<u-picker
:show="showHolidayPicker"
:columns="holidayColumns"
keyName="label"
@confirm="onHolidayConfirm"
@cancel="showHolidayPicker = false"
@close="showHolidayPicker = false"
></u-picker>
<u-picker
:show="showReasonPicker"
:columns="reasonColumns"
keyName="label"
@confirm="onReasonConfirm"
@cancel="showReasonPicker = false"
@close="showReasonPicker = false"
></u-picker>
<u-datetime-picker
:show="showDatePicker"
v-model="dateValue"
mode="datetime"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
@close="showDatePicker = false"
></u-datetime-picker>
</view>
</template>
<script>
import { applyHolidayStay } from '@/api/holidayStay.js';
import { getDict } from '@/api/system/dict/data.js';
import { request } from '@/common/request.js';
export default {
data() {
return {
submitting: false,
showHolidayPicker: false,
showReasonPicker: false,
showDatePicker: false,
dateField: 'start',
dateValue: Number(new Date()),
holidayTypeDict: [],
stayReasonTypeDict: [],
holidayTypeLabel: '',
stayReasonTypeLabel: '',
dormLoading: false,
dormText: '',
hasDorm: false,
studentId: null,
form: {
holidayType: '',
applyStartDate: '',
applyEndDate: '',
stayReasonType: 'other',
stayReason: '',
studentPhone: '',
emergencyContact: '',
emergencyPhone: '',
parentConsent: '1',
remark: ''
}
};
},
computed: {
holidayColumns() {
return [this.holidayTypeDict || []];
},
reasonColumns() {
return [this.stayReasonTypeDict || []];
}
},
async onLoad() {
try {
this.holidayTypeDict = (await getDict('xg_holiday_type')) || [];
this.stayReasonTypeDict = (await getDict('xg_stay_reason_type')) || [];
const reason = this.stayReasonTypeDict.find((d) => String(d.value) === 'other');
if (reason) {
this.stayReasonTypeLabel = reason.label;
this.form.stayReasonType = reason.value;
}
const stuRes = await request({
url: '/studentMsg/studentMsg/currentUserInfo',
method: 'GET'
});
const stu = stuRes.data || stuRes;
if (stu && stu.phone) {
this.form.studentPhone = stu.phone;
}
if (stu && stu.id) {
this.studentId = stu.id;
await this.loadDormInfo(stu.id);
}
} catch (e) {
// ignore
}
},
methods: {
goBack() {
uni.navigateBack();
},
async loadDormInfo(studentId) {
this.dormLoading = true;
this.hasDorm = false;
this.dormText = '';
try {
const res = await request({
url: `/dormBedStudent/dormBedStudent/currentDormInfo/${studentId}`,
method: 'GET'
});
const dorm = res.data || res;
if (dorm && (dorm.roomId || dorm.bedId || dorm.buildingName || dorm.roomName)) {
const parts = [
dorm.campusName,
dorm.buildingName,
dorm.roomName || dorm.roomNo,
dorm.bedName || dorm.bedNo
].filter(Boolean);
this.dormText = parts.join(' / ');
this.hasDorm = !!(dorm.roomId && dorm.bedId);
}
} catch (e) {
this.dormText = '';
this.hasDorm = false;
} finally {
this.dormLoading = false;
}
},
openHolidayPicker() {
if (!this.holidayTypeDict.length) {
uni.showToast({ title: '假期类型加载中,请稍后', icon: 'none' });
return;
}
this.showHolidayPicker = true;
},
openReasonPicker() {
if (!this.stayReasonTypeDict.length) {
uni.showToast({ title: '原因类型加载中,请稍后', icon: 'none' });
return;
}
this.showReasonPicker = true;
},
openDate(field) {
this.dateField = field;
const current = field === 'start' ? this.form.applyStartDate : this.form.applyEndDate;
this.dateValue = current ? new Date(current.replace(/-/g, '/')).getTime() : Number(new Date());
this.showDatePicker = true;
},
onHolidayConfirm(e) {
const item = (e.value && e.value[0]) || null;
if (item) {
this.form.holidayType = item.value;
this.holidayTypeLabel = item.label;
}
this.showHolidayPicker = false;
},
onReasonConfirm(e) {
const item = (e.value && e.value[0]) || null;
if (item) {
this.form.stayReasonType = item.value;
this.stayReasonTypeLabel = item.label;
}
this.showReasonPicker = false;
},
onDateConfirm(e) {
const ts = e && e.value !== undefined ? e.value : e;
const d = new Date(Number(ts));
if (Number.isNaN(d.getTime())) {
this.showDatePicker = false;
return;
}
const pad = (n) => (n < 10 ? '0' + n : '' + n);
const str =
d.getFullYear() +
'-' +
pad(d.getMonth() + 1) +
'-' +
pad(d.getDate()) +
' ' +
pad(d.getHours()) +
':' +
pad(d.getMinutes()) +
':' +
pad(d.getSeconds());
if (this.dateField === 'start') {
this.form.applyStartDate = str;
} else {
this.form.applyEndDate = str;
}
this.showDatePicker = false;
},
async submit() {
if (!this.hasDorm) {
return uni.showToast({ title: '未分配宿舍床位,无法申请', icon: 'none' });
}
if (!this.form.holidayType) {
return uni.showToast({ title: '请选择假期类型', icon: 'none' });
}
if (!this.form.applyStartDate || !this.form.applyEndDate) {
return uni.showToast({ title: '请选择留宿时间', icon: 'none' });
}
if (!this.form.stayReason || !this.form.stayReason.trim()) {
return uni.showToast({ title: '请填写留宿原因', icon: 'none' });
}
this.submitting = true;
try {
await applyHolidayStay({ ...this.form });
uni.showToast({ title: '提交成功', icon: 'success' });
setTimeout(() => {
uni.navigateBack();
}, 500);
} catch (e) {
uni.showToast({ title: (e && e.msg) || '提交失败', icon: 'none' });
} finally {
this.submitting = false;
}
}
}
};
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
}
.form-wrap {
margin: 24rpx;
padding: 24rpx;
background: #fff;
border-radius: 16rpx;
}
.tip {
margin-top: 24rpx;
font-size: 24rpx;
color: #909399;
line-height: 1.5;
}
.dorm-card {
margin-bottom: 24rpx;
padding: 20rpx;
background: #f7f8fa;
border-radius: 12rpx;
}
.dorm-title {
font-size: 26rpx;
color: #909399;
margin-bottom: 8rpx;
}
.dorm-text {
font-size: 28rpx;
color: #303133;
}
.dorm-warn {
color: #e6a23c;
}
.radio-group {
display: flex;
align-items: center;
}
.radio-item {
margin-right: 32rpx;
}
</style>
+201
View File
@@ -0,0 +1,201 @@
<template>
<view class="page-container">
<u-navbar title="我的留宿申请" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<view class="filter-bar">
<u-tabs
:list="statusTabs"
:current="tabIndex"
@change="onTabChange"
line-color="#3c9cff"
></u-tabs>
</view>
<u-loading-page :loading="loading && list.length === 0" loading-text="加载中..."></u-loading-page>
<view v-if="!loading || list.length > 0" class="content">
<u-empty v-if="list.length === 0" text="暂无申请记录" mode="data"></u-empty>
<view v-else class="card-list">
<view v-for="(item, index) in list" :key="item.id || index" class="card">
<view class="card-header">
<text class="title">{{ holidayTypeLabel(item.holidayType) }}</text>
<u-tag
:text="applyStatusLabel(item.applyStatus)"
:type="applyStatusType(item.applyStatus)"
size="mini"
></u-tag>
</view>
<view class="row"><text class="label">宿舍:</text><text>{{ item.buildingName }}{{ item.roomName }} {{ item.bedName }}</text></view>
<view class="row"><text class="label">时间:</text><text>{{ formatDate(item.applyStartDate) }} ~ {{ formatDate(item.applyEndDate) }}</text></view>
<view class="row"><text class="label">原因:</text><text>{{ item.stayReason || '-' }}</text></view>
<view class="row" v-if="item.auditOpinion"><text class="label">意见:</text><text>{{ item.auditOpinion }}</text></view>
<view class="row" v-if="item.revokeReason"><text class="label">撤销:</text><text>{{ item.revokeReason }}</text></view>
</view>
</view>
<view class="load-more" v-if="list.length > 0">
<u-loadmore :status="loadStatus" />
</view>
</view>
<view class="fab">
<u-button type="primary" shape="circle" text="申请留宿" @click="gotoApply"></u-button>
</view>
</view>
</template>
<script>
import { listMyHolidayStay } from '@/api/holidayStay.js';
import { getDict } from '@/api/system/dict/data.js';
export default {
data() {
return {
loading: false,
list: [],
total: 0,
pageNum: 1,
pageSize: 10,
loadStatus: 'loadmore',
tabIndex: 0,
applyStatus: '',
statusTabs: [
{ name: '全部', value: '' },
{ name: '待审', value: '1' },
{ name: '已通过', value: '2' },
{ name: '已驳回', value: '3' },
{ name: '已撤销', value: '4' }
],
holidayTypeDict: [],
applyStatusDict: []
};
},
onShow() {
this.refresh();
},
onReachBottom() {
this.loadMore();
},
async onLoad() {
try {
this.holidayTypeDict = (await getDict('xg_holiday_type')) || [];
this.applyStatusDict = (await getDict('xg_holiday_stay_apply_status')) || [];
} catch (e) {
// ignore
}
},
methods: {
goBack() {
uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/index/index' }) });
},
gotoApply() {
uni.navigateTo({ url: '/pages/xgss/jqzs/apply' });
},
onTabChange(item) {
const idx = typeof item === 'number' ? item : item.index;
this.tabIndex = idx;
this.applyStatus = this.statusTabs[idx].value;
this.refresh();
},
async refresh() {
this.pageNum = 1;
this.list = [];
await this.fetchList();
},
async loadMore() {
if (this.loadStatus !== 'loadmore') return;
this.pageNum += 1;
await this.fetchList(true);
},
async fetchList(append = false) {
this.loading = true;
this.loadStatus = 'loading';
try {
const res = await listMyHolidayStay({
pageNum: this.pageNum,
pageSize: this.pageSize,
applyStatus: this.applyStatus || undefined
});
const rows = res.rows || [];
this.total = res.total || 0;
this.list = append ? this.list.concat(rows) : rows;
this.loadStatus = this.list.length >= this.total ? 'nomore' : 'loadmore';
} catch (e) {
this.loadStatus = 'loadmore';
uni.showToast({ title: '加载失败', icon: 'none' });
} finally {
this.loading = false;
}
},
holidayTypeLabel(val) {
const hit = this.holidayTypeDict.find((d) => String(d.value) === String(val));
return (hit && hit.label) || val || '假期留宿';
},
applyStatusLabel(val) {
const hit = this.applyStatusDict.find((d) => String(d.value) === String(val));
return (hit && hit.label) || val || '-';
},
applyStatusType(val) {
const map = { '0': 'info', '1': 'warning', '2': 'success', '3': 'error', '4': 'info' };
return map[String(val)] || 'info';
},
formatDate(v) {
if (!v) return '-';
return String(v).replace('T', ' ').slice(0, 16);
}
}
};
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: 120rpx;
}
.filter-bar {
background: #fff;
}
.content {
padding: 24rpx;
}
.card-list .card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.title {
font-size: 30rpx;
font-weight: 600;
color: #303133;
}
.row {
font-size: 26rpx;
color: #606266;
margin-top: 8rpx;
display: flex;
}
.label {
color: #909399;
min-width: 90rpx;
}
.fab {
position: fixed;
left: 0;
right: 0;
bottom: 40rpx;
display: flex;
justify-content: center;
padding: 0 40rpx;
}
</style>
+283
View File
@@ -0,0 +1,283 @@
<template>
<view class="page-container">
<u-navbar title="留宿审批" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<view class="filter-bar">
<u-tabs
:list="statusTabs"
:current="tabIndex"
@change="onTabChange"
line-color="#3c9cff"
></u-tabs>
</view>
<view class="search-box">
<u-search
v-model="studentName"
placeholder="学生姓名"
:show-action="true"
action-text="搜索"
@search="refresh"
@custom="refresh"
@clear="refresh"
></u-search>
</view>
<u-loading-page :loading="loading && list.length === 0" loading-text="加载中..."></u-loading-page>
<view v-if="!loading || list.length > 0" class="content">
<u-empty v-if="list.length === 0" text="暂无本班申请" mode="data"></u-empty>
<view v-else class="card-list">
<view v-for="(item, index) in list" :key="item.id || index" class="card">
<view class="card-header">
<view>
<text class="name">{{ item.studentName }}</text>
<text class="no">{{ item.studentNo }}</text>
</view>
<u-tag
:text="applyStatusLabel(item.applyStatus)"
:type="applyStatusType(item.applyStatus)"
size="mini"
></u-tag>
</view>
<view class="row">班级:{{ item.className || '-' }}</view>
<view class="row">宿舍:{{ item.buildingName }}{{ item.roomName }} {{ item.bedName }}</view>
<view class="row">时间:{{ formatDate(item.applyStartDate) }} ~ {{ formatDate(item.applyEndDate) }}</view>
<view class="row">原因:{{ item.stayReason || '-' }}</view>
<view class="actions" v-if="item.applyStatus === '1'">
<u-button size="mini" type="success" text="通过" @click="doApprove(item)"></u-button>
<u-button size="mini" type="warning" text="驳回" @click="openReject(item)"></u-button>
</view>
<view class="actions" v-else-if="item.applyStatus === '2'">
<u-button size="mini" type="error" text="撤销" @click="openRevoke(item)"></u-button>
</view>
</view>
</view>
<u-loadmore v-if="list.length > 0" :status="loadStatus" />
</view>
<u-modal
:show="modal.show"
:title="modal.title"
showCancelButton
@confirm="submitModal"
@cancel="modal.show = false"
>
<view class="modal-body">
<u-textarea v-model="modal.opinion" :placeholder="modal.placeholder" maxlength="200" count></u-textarea>
</view>
</u-modal>
</view>
</template>
<script>
import {
listTeacherHolidayStay,
approveHolidayStay,
rejectHolidayStay,
revokeHolidayStay
} from '@/api/holidayStay.js';
import { getDict } from '@/api/system/dict/data.js';
export default {
data() {
return {
loading: false,
list: [],
total: 0,
pageNum: 1,
pageSize: 10,
loadStatus: 'loadmore',
tabIndex: 1,
applyStatus: '1',
studentName: '',
statusTabs: [
{ name: '全部', value: '' },
{ name: '待审', value: '1' },
{ name: '已通过', value: '2' },
{ name: '已驳回', value: '3' },
{ name: '已撤销', value: '4' }
],
applyStatusDict: [],
modal: {
show: false,
title: '',
mode: '',
id: null,
opinion: '',
placeholder: ''
}
};
},
onShow() {
this.refresh();
},
onReachBottom() {
this.loadMore();
},
async onLoad() {
try {
this.applyStatusDict = (await getDict('xg_holiday_stay_apply_status')) || [];
} catch (e) {
// ignore
}
},
methods: {
goBack() {
uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/index/index' }) });
},
onTabChange(item) {
const idx = typeof item === 'number' ? item : item.index;
this.tabIndex = idx;
this.applyStatus = this.statusTabs[idx].value;
this.refresh();
},
async refresh() {
this.pageNum = 1;
this.list = [];
await this.fetchList();
},
async loadMore() {
if (this.loadStatus !== 'loadmore') return;
this.pageNum += 1;
await this.fetchList(true);
},
async fetchList(append = false) {
this.loading = true;
this.loadStatus = 'loading';
try {
const res = await listTeacherHolidayStay({
pageNum: this.pageNum,
pageSize: this.pageSize,
applyStatus: this.applyStatus || undefined,
studentName: this.studentName || undefined
});
const rows = res.rows || [];
this.total = res.total || 0;
this.list = append ? this.list.concat(rows) : rows;
this.loadStatus = this.list.length >= this.total ? 'nomore' : 'loadmore';
} catch (e) {
this.loadStatus = 'loadmore';
uni.showToast({ title: '加载失败', icon: 'none' });
} finally {
this.loading = false;
}
},
async doApprove(item) {
try {
await approveHolidayStay({ id: item.id });
uni.showToast({ title: '已通过', icon: 'success' });
this.refresh();
} catch (e) {
uni.showToast({ title: (e && e.msg) || '操作失败', icon: 'none' });
}
},
openReject(item) {
this.modal = {
show: true,
title: '驳回申请',
mode: 'reject',
id: item.id,
opinion: '',
placeholder: '请填写驳回意见(选填)'
};
},
openRevoke(item) {
this.modal = {
show: true,
title: '撤销留宿',
mode: 'revoke',
id: item.id,
opinion: '',
placeholder: '请填写撤销原因(必填)'
};
},
async submitModal() {
if (this.modal.mode === 'revoke' && !String(this.modal.opinion || '').trim()) {
uni.showToast({ title: '请填写撤销原因', icon: 'none' });
return;
}
try {
if (this.modal.mode === 'reject') {
await rejectHolidayStay({ id: this.modal.id, auditOpinion: this.modal.opinion });
uni.showToast({ title: '已驳回', icon: 'success' });
} else {
await revokeHolidayStay({ id: this.modal.id, revokeReason: this.modal.opinion });
uni.showToast({ title: '已撤销', icon: 'success' });
}
this.modal.show = false;
this.refresh();
} catch (e) {
uni.showToast({ title: (e && e.msg) || '操作失败', icon: 'none' });
}
},
applyStatusLabel(val) {
const hit = this.applyStatusDict.find((d) => String(d.value) === String(val));
return (hit && hit.label) || val || '-';
},
applyStatusType(val) {
const map = { '0': 'info', '1': 'warning', '2': 'success', '3': 'error', '4': 'info' };
return map[String(val)] || 'info';
},
formatDate(v) {
if (!v) return '-';
return String(v).replace('T', ' ').slice(0, 16);
}
}
};
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
}
.filter-bar,
.search-box {
background: #fff;
}
.search-box {
padding: 16rpx 24rpx;
}
.content {
padding: 24rpx;
}
.card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.name {
font-size: 30rpx;
font-weight: 600;
margin-right: 12rpx;
}
.no {
font-size: 24rpx;
color: #909399;
}
.row {
font-size: 26rpx;
color: #606266;
margin-top: 6rpx;
}
.actions {
margin-top: 20rpx;
display: flex;
gap: 16rpx;
}
.modal-body {
padding: 20rpx;
}
</style>
@@ -12,8 +12,9 @@
**主要功能模块:**
#### 1.1 宿舍整体情况
- 违纪类型选择(从API加载)
- 宿舍扣分输入(0-100分)
- 扣分项多选(违纪类型)
- **归属班级选择(必填)**:选项为当前寝室在住学生班级(去重),默认选中第一项
- 宿舍扣分(由扣分项合计,只读,0-100分)
- 卫生等级选择(字典:`xg_dorm_hygiene_grade`)
- 检查附件上传(支持图片,最多15个)
@@ -83,6 +83,32 @@
</view>
</u-popup>
<!-- 归属班级(本寝室在住学生班级) -->
<u-form-item label="归属班级" prop="classId" required>
<view
@click="!isViewMode && classOptions.length && (showClassPicker = true)"
style="width: 100%"
>
<u-input
:value="classDisplay"
:placeholder="classOptions.length ? '请选择归属班级' : '当前寝室无在住学生班级'"
readonly
border="none"
:suffix-icon="isViewMode || !classOptions.length ? '' : 'arrow-down'"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
<u-picker
v-if="!isViewMode"
:show="showClassPicker && classOptions.length > 0"
:columns="classColumns"
keyName="text"
@confirm="handleClassConfirm"
@cancel="showClassPicker = false"
></u-picker>
</u-form-item>
<!-- 宿舍扣分 -->
<u-form-item label="宿舍扣分" prop="totalScore">
<u-input
@@ -634,6 +660,8 @@ export default {
formData: {
disciplineTypeId: null,
disciplineTypeName: '',
classId: null,
className: '',
totalScore: 0,
hygieneGrade: 'A',
checkRemarks: '',
@@ -643,9 +671,12 @@ export default {
deductionItems: [],
showDeductionPopup: false,
tempSelectedTypeIds: [],
classOptions: [],
showClassPicker: false,
// 表单验证规则
rules: {
classId: [{ required: true, message: '请选择归属班级', trigger: 'change' }],
hygieneGrade: [{ required: true, message: '请选择卫生等级', trigger: 'change' }],
},
@@ -718,6 +749,19 @@ export default {
return this.deductionItems.map((item) => item.disciplineTypeName).join('、');
},
classColumns() {
return [
this.classOptions.map((item) => ({
text: item.className,
value: item.classId,
})),
];
},
classDisplay() {
return this.formData.className || '';
},
// 卫生等级列
hygieneGradeColumns() {
return [
@@ -919,6 +963,7 @@ export default {
async loadRoomStudents() {
if (!this.roomInfo || !this.roomInfo.id) {
this.roomStudents = [];
this.syncClassOptionsFromStudents([]);
return;
}
@@ -946,21 +991,74 @@ export default {
showAttendancePicker: false,
showGradePicker: false,
}));
this.syncClassOptionsFromStudents(this.roomStudents);
// 加载学生请假信息
await this.loadStudentLeaves();
} else {
this.roomStudents = [];
this.syncClassOptionsFromStudents([]);
}
} catch (error) {
console.error('加载房间学生信息失败:', error);
uni.showToast({ title: '加载学生信息失败', icon: 'none' });
this.roomStudents = [];
this.syncClassOptionsFromStudents([]);
} finally {
this.studentsLoading = false;
}
},
/** 从本房学生提取班级选项,默认选中第一项 */
syncClassOptionsFromStudents(students, preferClassId) {
const map = new Map();
(students || []).forEach((student) => {
if (
student.classId == null ||
student.classId === '' ||
student.classId === 0 ||
student.classId === '0'
) {
return;
}
const key = String(student.classId);
if (!map.has(key)) {
map.set(key, {
classId: student.classId,
className: student.className || key,
});
}
});
this.classOptions = Array.from(map.values());
if (!this.classOptions.length) {
this.formData.classId = null;
this.formData.className = '';
return;
}
const preferred =
preferClassId !== undefined && preferClassId !== null
? preferClassId
: this.formData.classId;
const matched =
preferred != null
? this.classOptions.find((item) => String(item.classId) === String(preferred))
: null;
const selected = matched || this.classOptions[0];
this.formData.classId = selected.classId;
this.formData.className = selected.className;
},
handleClassConfirm(e) {
const selected = e.value && e.value[0];
if (selected) {
this.formData.classId = selected.value;
this.formData.className = selected.text;
}
this.showClassPicker = false;
},
// 加载学生请假信息
async loadStudentLeaves() {
if (!this.checkInfo || !this.checkInfo.checkDate || this.roomStudents.length === 0)
@@ -1008,6 +1106,8 @@ export default {
// 1️⃣ 回显表单基础数据
this.formData.disciplineTypeId = this.existingData.disciplineTypeId || null;
this.formData.disciplineTypeName = this.existingData.disciplineTypeName || '';
this.formData.classId = this.existingData.classId || null;
this.formData.className = this.existingData.className || '';
this.formData.totalScore = this.existingData.totalScore || 0;
this.formData.hygieneGrade = this.existingData.hygieneGrade || 'A';
this.formData.checkRemarks = this.existingData.checkRemarks || '';
@@ -1060,6 +1160,7 @@ export default {
studentId: normalizeStudentId(studentScore.studentId),
studentName: studentScore.studentName,
studentNo: studentScore.studentNo,
classId: studentScore.classId,
className: studentScore.className,
majorName: studentScore.majorName,
collegeName: studentScore.collegeName,
@@ -1091,6 +1192,7 @@ export default {
}
this.roomStudents = students;
this.syncClassOptionsFromStudents(students, this.formData.classId);
console.log('✅ 学生评分数据映射完成');
@@ -2372,6 +2474,20 @@ export default {
return;
}
if (!this.formData.classId) {
uni.showToast({
title: '请选择归属班级(取自当前寝室在住学生)',
icon: 'none',
});
return;
}
const matchedClass = this.classOptions.find(
(item) => String(item.classId) === String(this.formData.classId),
);
if (matchedClass) {
this.formData.className = matchedClass.className;
}
this.submitLoading = true;
this.syncStudentScoreFromViolations();
@@ -2387,8 +2503,11 @@ export default {
studentId: student.studentId,
studentName: student.studentName,
studentNo: student.studentNo,
classId: student.classId,
className: student.className,
majorId: student.majorId,
majorName: student.majorName,
collegeId: student.collegeId,
collegeName: student.collegeName,
bedId: student.bedId,
bedCode: student.bedCode,
@@ -84,6 +84,16 @@ export interface DormHygieneCheckVO {
*/
campusName: string;
/**
* 归属班级ID(宿舍扣分归属)
*/
classId?: string | number;
/**
* 归属班级名称
*/
className?: string;
/**
* 宿舍总分(0-100分)
*/
@@ -227,6 +237,16 @@ export interface DormHygieneCheckForm extends BaseEntity {
*/
campusName?: string;
/**
* 归属班级ID(宿舍扣分归属)
*/
classId?: string | number;
/**
* 归属班级名称
*/
className?: string;
/**
* 宿舍总分(0-100分)
*/
@@ -142,3 +142,57 @@ export const getBedOptions = (roomId?: string | number): AxiosPromise<any[]> =>
params: roomId ? { roomId } : undefined
});
};
/** 学生提交留宿申请 */
export const applyHolidayStay = (data: HolidayStayForm) => {
return request({
url: '/holidayStay/holidayStay/apply',
method: 'post',
data
});
};
/** 当前学生申请列表 */
export const listMyHolidayStay = (query?: HolidayStayQuery): AxiosPromise<HolidayStayVO[]> => {
return request({
url: '/holidayStay/holidayStay/myList',
method: 'get',
params: query
});
};
/** 班主任本班申请列表 */
export const listTeacherHolidayStay = (query?: HolidayStayQuery): AxiosPromise<HolidayStayVO[]> => {
return request({
url: '/holidayStay/holidayStay/teacherList',
method: 'get',
params: query
});
};
/** 班主任审批通过 */
export const approveHolidayStay = (data: { id: string | number; auditOpinion?: string }) => {
return request({
url: '/holidayStay/holidayStay/approve',
method: 'post',
data
});
};
/** 班主任驳回 */
export const rejectHolidayStay = (data: { id: string | number; auditOpinion?: string }) => {
return request({
url: '/holidayStay/holidayStay/reject',
method: 'post',
data
});
};
/** 班主任撤销已通过申请 */
export const revokeHolidayStay = (data: { id: string | number; revokeReason: string }) => {
return request({
url: '/holidayStay/holidayStay/revoke',
method: 'post',
data
});
};
@@ -173,6 +173,56 @@ export interface HolidayStayVO {
* 备注信息
*/
remark: string;
/**
* 申请状态(0草稿 1待审 2已通过 3已驳回 4已撤销)
*/
applyStatus?: string;
/**
* 提交申请时间
*/
applyTime?: string;
/**
* 审批人用户ID
*/
auditorId?: string | number;
/**
* 审批人姓名
*/
auditorName?: string;
/**
* 审批时间
*/
auditTime?: string;
/**
* 审批意见
*/
auditOpinion?: string;
/**
* 撤销人用户ID
*/
revokeBy?: string | number;
/**
* 撤销人姓名
*/
revokeByName?: string;
/**
* 撤销时间
*/
revokeTime?: string;
/**
* 撤销原因
*/
revokeReason?: string;
}
export interface HolidayStayForm extends BaseEntity {
@@ -350,6 +400,11 @@ export interface HolidayStayForm extends BaseEntity {
* 备注信息
*/
remark?: string;
/**
* 申请状态(0草稿 1待审 2已通过 3已驳回 4已撤销)
*/
applyStatus?: string;
}
export interface HolidayStayQuery extends PageQuery {
@@ -523,6 +578,11 @@ export interface HolidayStayQuery extends PageQuery {
*/
status?: string;
/**
* 申请状态(0草稿 1待审 2已通过 3已驳回 4已撤销)
*/
applyStatus?: string;
/**
* 日期范围参数
*/
@@ -57,6 +57,25 @@
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="归属班级" prop="classId">
<el-select
v-model="hygieneForm.classId"
:placeholder="classOptions.length ? '请选择归属班级' : '当前寝室无在住学生班级'"
style="width: 100%"
filterable
:disabled="!classOptions.length"
@change="handleClassChange"
>
<el-option
v-for="item in classOptions"
:key="String(item.classId)"
:label="item.className"
:value="item.classId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="宿舍扣分" prop="totalScore">
<el-input-number
@@ -407,6 +426,8 @@ const hygieneForm = reactive({
campusName: '',
disciplineTypeId: null as number | null,
disciplineTypeName: '',
classId: undefined as string | number | undefined,
className: '',
totalScore: 0,
hygieneGrade: 'A',
checkRemarks: '',
@@ -418,6 +439,7 @@ const hygieneRules = reactive({
checkTitle: [{ required: true, message: '检查标题不能为空', trigger: 'blur' }],
checkDate: [{ required: true, message: '检查日期不能为空', trigger: 'change' }],
checkerName: [{ required: true, message: '检查员不能为空', trigger: 'blur' }],
classId: [{ required: true, message: '请选择归属班级', trigger: 'change' }],
totalScore: [
{
validator: (rule: any, value: any, callback: (error?: string | Error) => void) => {
@@ -440,6 +462,8 @@ const hygieneRules = reactive({
// 学生评分数据
const studentScores = ref<any[]>([]);
/** 本寝室在住学生班级(去重) */
const classOptions = ref<Array<{ classId: string | number; className: string }>>([]);
// 违纪记录数据(使用普通对象,避免 Map 变更无法触发 Vue 响应式更新)
const violationRecords = ref<Record<string, any[]>>({});
@@ -488,6 +512,41 @@ const handleDeductionTotalChange = (total: number) => {
hygieneForm.totalScore = total;
};
/** 从本房学生提取班级选项,并默认选中第一项(已选且仍有效则保留) */
const syncClassOptionsFromStudents = (students: any[], preferClassId?: string | number | null) => {
const map = new Map<string, { classId: string | number; className: string }>();
students.forEach((student) => {
if (student.classId == null || student.classId === '' || student.classId === 0 || student.classId === '0') {
return;
}
const key = String(student.classId);
if (!map.has(key)) {
map.set(key, {
classId: student.classId,
className: student.className || key
});
}
});
classOptions.value = Array.from(map.values());
if (!classOptions.value.length) {
hygieneForm.classId = undefined;
hygieneForm.className = '';
return;
}
const preferred = preferClassId ?? hygieneForm.classId;
const matched = preferred != null ? classOptions.value.find((item) => String(item.classId) === String(preferred)) : undefined;
const selected = matched || classOptions.value[0];
hygieneForm.classId = selected.classId;
hygieneForm.className = selected.className;
};
const handleClassChange = (classId: string | number) => {
const matched = classOptions.value.find((item) => String(item.classId) === String(classId));
hygieneForm.className = matched?.className || '';
};
// 上传配置
const uploadAction = computed(() => {
return (import.meta as any).env.VITE_APP_BASE_API + '/resource/oss/upload';
@@ -623,14 +682,38 @@ const loadStudentScores = async () => {
studentScores.value = scores;
console.log('学生评分数据加载成功:', scores.length, '条');
// 班级选项优先用评分中的班级;若缺失则从当前寝室在住学生补全
let classSource = scores;
const hasClass = scores.some(
(s: any) => s.classId != null && s.classId !== '' && s.classId !== 0 && s.classId !== '0'
);
if (!hasClass && hygieneForm.roomId) {
try {
const roomRes = await request({
url: '/dormBedStudent/dormBedStudent/getRoomStudents',
method: 'get',
params: { roomId: hygieneForm.roomId }
});
if (roomRes.code === 200 && roomRes.data) {
classSource = roomRes.data;
}
} catch (error) {
console.warn('加载寝室班级选项失败', error);
}
}
syncClassOptionsFromStudents(classSource, hygieneForm.classId);
await loadStudentLeaves();
} else {
studentScores.value = [];
syncClassOptionsFromStudents([], hygieneForm.classId);
}
} catch (error) {
console.error('加载学生评分数据失败:', error);
ElMessage.error('加载学生评分数据失败');
studentScores.value = [];
syncClassOptionsFromStudents([], hygieneForm.classId);
} finally {
studentsLoading.value = false;
}
@@ -1179,6 +1262,12 @@ const submitForm = async () => {
return;
}
if (!hygieneForm.classId) {
ElMessage.error('请选择归属班级(取自当前寝室在住学生)');
return;
}
handleClassChange(hygieneForm.classId);
buttonLoading.value = true;
// 提交前同步个人扣分为违纪合计
@@ -1194,8 +1283,11 @@ const submitForm = async () => {
studentId: student.studentId,
studentName: student.studentName,
studentNo: student.studentNo,
classId: student.classId,
className: student.className,
majorId: student.majorId,
majorName: student.majorName,
collegeId: student.collegeId,
collegeName: student.collegeName,
bedId: student.bedId,
bedCode: student.bedCode,
@@ -22,6 +22,25 @@
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="归属班级" prop="classId">
<el-select
v-model="hygieneForm.classId"
:placeholder="classOptions.length ? '请选择归属班级' : '当前寝室无在住学生班级'"
style="width: 100%"
filterable
:disabled="!classOptions.length"
@change="handleClassChange"
>
<el-option
v-for="item in classOptions"
:key="String(item.classId)"
:label="item.className"
:value="item.classId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="宿舍扣分" prop="totalScore">
<el-input-number
@@ -336,6 +355,8 @@ const deductionItems = ref<HygieneDeductionItem[]>([]);
const hygieneForm = reactive({
disciplineTypeId: null as number | null,
disciplineTypeName: '',
classId: undefined as string | number | undefined,
className: '',
totalScore: 0,
hygieneGrade: 'A',
checkRemarks: '',
@@ -344,6 +365,7 @@ const hygieneForm = reactive({
// 表单验证规则
const hygieneRules = reactive({
classId: [{ required: true, message: '请选择归属班级', trigger: 'change' }],
totalScore: [
{
validator: (rule: any, value: any, callback: (error?: string | Error) => void) => {
@@ -366,6 +388,8 @@ const hygieneRules = reactive({
// 学生相关数据
const roomStudents = ref<any[]>([]);
/** 本寝室在住学生班级(去重) */
const classOptions = ref<Array<{ classId: string | number; className: string }>>([]);
const violationRecords = ref<Map<string | number, any[]>>(new Map());
const studentLeaves = ref<Map<string | number, any>>(new Map());
@@ -444,10 +468,48 @@ const handleDeductionTotalChange = (total: number) => {
hygieneForm.totalScore = total;
};
/** 从本房学生提取班级选项,并默认选中第一项(已选且仍有效则保留) */
const syncClassOptionsFromStudents = (students: any[], preferClassId?: string | number | null) => {
const map = new Map<string, { classId: string | number; className: string }>();
students.forEach((student) => {
const classId = student.classId;
const hasClassId = classId != null && classId !== '' && classId !== 0 && classId !== '0';
if (!hasClassId) {
return;
}
const key = String(classId);
if (!map.has(key)) {
map.set(key, {
classId,
className: (student.className || '').toString().trim() || key
});
}
});
classOptions.value = Array.from(map.values());
if (!classOptions.value.length) {
hygieneForm.classId = undefined;
hygieneForm.className = '';
return;
}
const preferred = preferClassId ?? hygieneForm.classId;
const matched = preferred != null ? classOptions.value.find((item) => String(item.classId) === String(preferred)) : undefined;
const selected = matched || classOptions.value[0];
hygieneForm.classId = selected.classId;
hygieneForm.className = selected.className;
};
const handleClassChange = (classId: string | number) => {
const matched = classOptions.value.find((item) => String(item.classId) === String(classId));
hygieneForm.className = matched?.className || '';
};
// 加载房间学生信息
const loadRoomStudents = async () => {
if (!props.roomInfo?.id) {
roomStudents.value = [];
syncClassOptionsFromStudents([]);
return;
}
@@ -467,16 +529,19 @@ const loadRoomStudents = async () => {
attendance_status: '1',
student_grade: 'A'
}));
syncClassOptionsFromStudents(roomStudents.value);
// 加载学生请假信息
await loadStudentLeaves();
} else {
roomStudents.value = [];
syncClassOptionsFromStudents([]);
}
} catch (error) {
console.error('加载房间学生信息失败:', error);
ElMessage.error('加载学生信息失败');
roomStudents.value = [];
syncClassOptionsFromStudents([]);
} finally {
studentsLoading.value = false;
}
@@ -878,6 +943,12 @@ const submitForm = async () => {
return;
}
if (!hygieneForm.classId) {
ElMessage.error('请选择归属班级(取自当前寝室在住学生)');
return;
}
handleClassChange(hygieneForm.classId);
buttonLoading.value = true;
syncStudentScoreFromViolations();
@@ -891,8 +962,11 @@ const submitForm = async () => {
studentId: student.studentId,
studentName: student.studentName,
studentNo: student.studentNo,
classId: student.classId,
className: student.className,
majorId: student.majorId,
majorName: student.majorName,
collegeId: student.collegeId,
collegeName: student.collegeName,
bedId: student.bedId,
bedCode: student.bedCode,
@@ -999,12 +1073,15 @@ const resetForm = () => {
Object.assign(hygieneForm, {
disciplineTypeId: null,
disciplineTypeName: '',
classId: undefined,
className: '',
totalScore: 0,
hygieneGrade: 'A',
checkRemarks: '',
attachmentIds: ''
});
deductionItems.value = [];
classOptions.value = [];
roomStudents.value = [];
violationRecords.value.clear();
@@ -105,6 +105,7 @@
</template>
</el-table-column>
<el-table-column label="房间名称" align="center" prop="roomName" />
<el-table-column label="归属班级" align="center" prop="className" min-width="140" show-overflow-tooltip />
<el-table-column label="宿舍楼名称" align="center" prop="buildingName" />
<el-table-column label="校区名称" align="center" prop="campusName" />
<el-table-column label="扣分项" align="center" prop="disciplineTypeName" min-width="160" show-overflow-tooltip />
@@ -16,6 +16,11 @@
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="申请状态" prop="applyStatus">
<el-select v-model="queryParams.applyStatus" placeholder="请选择申请状态" clearable>
<el-option v-for="dict in xg_holiday_stay_apply_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="dict in xg_holiday_stay_status" :key="dict.value" :label="dict.label" :value="dict.value" />
@@ -223,6 +228,11 @@
<el-table-column label="扩展字段2" align="center" prop="extField2" v-if="false" />
<el-table-column label="扩展字段3" align="center" prop="extField3" v-if="false" />
<el-table-column label="备注信息" align="center" prop="remark" />
<el-table-column label="申请状态" align="center" prop="applyStatus" width="100">
<template #default="scope">
<dict-tag :options="xg_holiday_stay_apply_status" :value="scope.row.applyStatus" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<el-tag :type="getStatusType(scope.row)" size="small">
@@ -801,8 +811,8 @@ import {
} from '@/api/xg/xgss/holidayStay/holidayStay/types-sg';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { xg_holiday_type, xg_stay_reason_type, sys_yesno, xg_holiday_stay_status } = toRefs<any>(
proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type', 'sys_yesno', 'xg_holiday_stay_status')
const { xg_holiday_type, xg_stay_reason_type, sys_yesno, xg_holiday_stay_status, xg_holiday_stay_apply_status } = toRefs<any>(
proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type', 'sys_yesno', 'xg_holiday_stay_status', 'xg_holiday_stay_apply_status')
);
// 用户store - 用于获取当前登录用户信息,实现权限控制
@@ -920,6 +930,7 @@ const data = reactive<PageData<HolidayStayForm, HolidayStayQuery>>({
extField1: undefined,
extField2: undefined,
extField3: undefined,
applyStatus: undefined,
status: 'valid', // 默认展示有效状态的数据
params: {}
},
@@ -1210,15 +1221,22 @@ const handleDelete = async (row?: HolidayStayVO) => {
await getList();
};
/** 导出按钮操作 */
/** 导出按钮操作:未选申请状态时默认导出已通过名单 */
const handleExport = () => {
proxy?.download(
'holidayStay/holidayStay/export',
{
...queryParams.value
},
`holidayStay_${new Date().getTime()}.xlsx`
);
const exportParams: any = { ...queryParams.value };
if (!exportParams.applyStatus) {
exportParams.applyStatus = '2';
}
if (exportParams.status === 'valid' || exportParams.status === 'invalid') {
const currentTime = new Date().toISOString().slice(0, 19).replace('T', ' ');
exportParams.params = {
...exportParams.params,
statusType: exportParams.status,
currentTime
};
delete exportParams.status;
}
proxy?.download('holidayStay/holidayStay/export', exportParams, `holidayStay_${new Date().getTime()}.xlsx`);
};
/** 远程搜索学生 */
@@ -13,8 +13,13 @@
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-form-item label="申请状态" prop="applyStatus">
<el-select v-model="queryParams.applyStatus" placeholder="请选择申请状态" clearable>
<el-option v-for="dict in xg_holiday_stay_apply_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="有效期" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择有效期" clearable>
<el-option label="有效" value="valid" />
<el-option label="失效" value="invalid" />
</el-select>
@@ -157,9 +162,11 @@
>删除</el-button
>
</el-col> -->
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['holidayStay:holidayStay:export']">导出</el-button>
</el-col> -->
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['holidayStay:holidayStay:export']"
>导出留宿名单</el-button
>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -221,15 +228,47 @@
<el-table-column label="扩展字段2" align="center" prop="extField2" v-if="false" />
<el-table-column label="扩展字段3" align="center" prop="extField3" v-if="false" />
<el-table-column label="备注信息" align="center" prop="remark" />
<el-table-column label="状态" align="center" prop="status" width="90">
<el-table-column label="申请状态" align="center" prop="applyStatus" width="100">
<template #default="scope">
<dict-tag :options="xg_holiday_stay_apply_status" :value="scope.row.applyStatus" />
</template>
</el-table-column>
<el-table-column label="有效期" align="center" prop="status" width="90">
<template #default="scope">
<el-tag :type="getStatusType(scope.row)" size="small">
{{ getStatusText(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="100">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="220">
<template #default="scope">
<el-tooltip content="通过" placement="top" v-if="scope.row.applyStatus === '1'">
<el-button
link
type="success"
@click="openAuditDialog(scope.row, 'approve')"
v-hasPermi="['holidayStay:holidayStay:audit']"
>通过</el-button
>
</el-tooltip>
<el-tooltip content="驳回" placement="top" v-if="scope.row.applyStatus === '1'">
<el-button
link
type="warning"
@click="openAuditDialog(scope.row, 'reject')"
v-hasPermi="['holidayStay:holidayStay:audit']"
>驳回</el-button
>
</el-tooltip>
<el-tooltip content="撤销" placement="top" v-if="scope.row.applyStatus === '2'">
<el-button
link
type="danger"
@click="openAuditDialog(scope.row, 'revoke')"
v-hasPermi="['holidayStay:holidayStay:revoke']"
>撤销</el-button
>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['holidayStay:holidayStay:edit']"></el-button>
</el-tooltip>
@@ -553,6 +592,26 @@
</div>
</template>
</el-dialog>
<!-- 审批 / 撤销 -->
<el-dialog :title="auditDialog.title" v-model="auditDialog.visible" width="480px" append-to-body>
<el-form label-width="100px">
<el-form-item :label="auditDialog.mode === 'revoke' ? '撤销原因' : '审批意见'" :required="auditDialog.mode === 'revoke'">
<el-input
v-model="auditDialog.opinion"
type="textarea"
:rows="4"
:placeholder="auditDialog.mode === 'revoke' ? '请输入撤销原因(必填)' : '请输入审批意见(选填)'"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="auditDialog.loading" type="primary" @click="submitAudit">确 定</el-button>
<el-button @click="auditDialog.visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -565,9 +624,8 @@ import { UploadFilled, Document, Loading, Download, Delete } from '@element-plus
import { getToken } from '@/utils/auth';
import ImagePreview from '@/components/ImagePreview/index.vue';
import request from '@/utils/request';
import { useUserStore } from '@/store/modules/user';
import {
listHolidayStay,
listTeacherHolidayStay,
getHolidayStay,
delHolidayStay,
addHolidayStay,
@@ -577,7 +635,10 @@ import {
getAreaOptions,
getBuildingOptions,
getRoomOptions,
getBedOptions
getBedOptions,
approveHolidayStay,
rejectHolidayStay,
revokeHolidayStay
} from '@/api/xg/xgss/holidayStay/holidayStay';
import {
HolidayStayVO,
@@ -593,10 +654,9 @@ import {
} from '@/api/xg/xgss/holidayStay/holidayStay/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { xg_holiday_type, xg_stay_reason_type, sys_yesno } = toRefs<any>(proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type', 'sys_yesno'));
// 用户store - 用于获取当前登录用户信息,实现权限控制
const userStore = useUserStore();
const { xg_holiday_type, xg_stay_reason_type, sys_yesno, xg_holiday_stay_apply_status } = toRefs<any>(
proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type', 'sys_yesno', 'xg_holiday_stay_apply_status')
);
const holidayStayList = ref<HolidayStayVO[]>([]);
const buttonLoading = ref(false);
@@ -701,6 +761,7 @@ const data = reactive<PageData<HolidayStayForm, HolidayStayQuery>>({
extField1: undefined,
extField2: undefined,
extField3: undefined,
applyStatus: '1', // 班主任默认看待审
status: 'valid', // 默认显示有效状态的记录
params: {}
},
@@ -734,34 +795,23 @@ const uploadData = computed(() => {
};
});
/** 查询假期留宿管理列表 */
/** 查询假期留宿管理列表(班主任本班) */
const getList = async () => {
loading.value = true;
// 确保用户信息已加载
if (!userStore.userId) {
await userStore.getInfo();
}
// 处理状态过滤逻辑
const requestParams = { ...queryParams.value };
// 添加创建人过滤条件 - 班主任只能看到自己创建的数据
(requestParams as any).createBy = userStore.userId;
// 根据状态设置时间过滤条件
// 根据有效期设置时间过滤条件
if (requestParams.status) {
const currentTime = new Date().toISOString().slice(0, 19).replace('T', ' ');
if (requestParams.status === 'valid') {
// 查询有效状态:结束时间大于等于当前时间
requestParams.params = {
...requestParams.params,
statusType: 'valid',
currentTime: currentTime
};
} else if (requestParams.status === 'invalid') {
// 查询失效状态:结束时间小于当前时间
requestParams.params = {
...requestParams.params,
statusType: 'invalid',
@@ -769,11 +819,10 @@ const getList = async () => {
};
}
// 移除status参数,因为后端不需要这个字段
delete requestParams.status;
}
const res = await listHolidayStay(requestParams);
const res = await listTeacherHolidayStay(requestParams);
holidayStayList.value = res.rows;
total.value = res.total;
loading.value = false;
@@ -911,15 +960,64 @@ const handleDelete = async (row?: HolidayStayVO) => {
await getList();
};
/** 导出按钮操作 */
/** 导出按钮操作:默认导出已通过名单;若已选申请状态则按筛选导出 */
const handleExport = () => {
proxy?.download(
'holidayStay/holidayStay/export',
{
...queryParams.value
},
`holidayStay_${new Date().getTime()}.xlsx`
);
const exportParams: any = { ...queryParams.value };
if (!exportParams.applyStatus) {
exportParams.applyStatus = '2';
}
if (exportParams.status) {
const currentTime = new Date().toISOString().slice(0, 19).replace('T', ' ');
exportParams.params = {
...exportParams.params,
statusType: exportParams.status,
currentTime
};
delete exportParams.status;
}
proxy?.download('holidayStay/holidayStay/export', exportParams, `holidayStay_${new Date().getTime()}.xlsx`);
};
const auditDialog = reactive({
visible: false,
title: '',
mode: 'approve' as 'approve' | 'reject' | 'revoke',
rowId: undefined as string | number | undefined,
opinion: '',
loading: false
});
const openAuditDialog = (row: HolidayStayVO, mode: 'approve' | 'reject' | 'revoke') => {
auditDialog.mode = mode;
auditDialog.rowId = row.id;
auditDialog.opinion = '';
auditDialog.title = mode === 'approve' ? '审批通过' : mode === 'reject' ? '审批驳回' : '撤销留宿';
auditDialog.visible = true;
};
const submitAudit = async () => {
if (!auditDialog.rowId) return;
if (auditDialog.mode === 'revoke' && !auditDialog.opinion?.trim()) {
proxy?.$modal.msgWarning('请填写撤销原因');
return;
}
auditDialog.loading = true;
try {
if (auditDialog.mode === 'approve') {
await approveHolidayStay({ id: auditDialog.rowId, auditOpinion: auditDialog.opinion });
proxy?.$modal.msgSuccess('已通过');
} else if (auditDialog.mode === 'reject') {
await rejectHolidayStay({ id: auditDialog.rowId, auditOpinion: auditDialog.opinion });
proxy?.$modal.msgSuccess('已驳回');
} else {
await revokeHolidayStay({ id: auditDialog.rowId, revokeReason: auditDialog.opinion });
proxy?.$modal.msgSuccess('已撤销');
}
auditDialog.visible = false;
await getList();
} finally {
auditDialog.loading = false;
}
};
/** 远程搜索学生 */