提交代码
This commit is contained in:
1 parent
1db2f91af1
commit
574660ce9f
20 files changed
+2790
-144
No files matched your search
+15
@@ -139,4 +139,19 @@ public class XgDormBedController extends BaseController {
|
||||
List<XgDormBedDetailVo> list = xgDormBedService.queryBedDetailsByBuildingId(buildingId);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定房间中属于指定班级的床位详细信息列表(包含学生信息)
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param classId 班级ID
|
||||
*/
|
||||
@SaCheckPermission("dormbed:dormBed:query")
|
||||
@GetMapping("/details/room/{roomId}/class/{classId}")
|
||||
public R<List<XgDormBedDetailVo>> getBedDetailsByRoomIdAndClassId(
|
||||
@NotNull(message = "房间ID不能为空") @PathVariable Long roomId,
|
||||
@NotNull(message = "班级ID不能为空") @PathVariable Long classId) {
|
||||
List<XgDormBedDetailVo> list = xgDormBedService.queryBedDetailsByRoomIdAndClassId(roomId, classId);
|
||||
return R.ok(list);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -25,6 +25,7 @@ import org.dromara.xg.service.IXgDormBedStudentService;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.xg.domain.bo.XgDormBedBatchRecallBo;
|
||||
import org.dromara.xg.domain.vo.DormBuildingTreeVo;
|
||||
import org.dromara.xg.domain.bo.XgDormSelectiveAllocationBo;
|
||||
|
||||
/**
|
||||
* 床位学生分配关系
|
||||
@@ -228,7 +229,7 @@ public class XgDormBedStudentController extends BaseController {
|
||||
@Log(title = "班主任选择性分配床位", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping("/classTeacherSelectiveAllocation")
|
||||
public R<Void> classTeacherSelectiveAllocation(@RequestBody Object allocationParams,
|
||||
public R<Void> classTeacherSelectiveAllocation(@Validated @RequestBody XgDormSelectiveAllocationBo allocationParams,
|
||||
@RequestParam(required = false) Long classId) {
|
||||
return toAjax(xgDormBedStudentService.classTeacherSelectiveAllocation(allocationParams, classId));
|
||||
}
|
||||
|
||||
+12
@@ -54,6 +54,18 @@ public class XgDormRoomController extends BaseController {
|
||||
return xgDormRoomService.queryPageListByCurrentUserCollege(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定班级的宿舍房间信息列表
|
||||
*/
|
||||
@SaCheckPermission("xgss:dormRoom:list")
|
||||
@GetMapping("/class/{classId}/list")
|
||||
public TableDataInfo<XgDormRoomVo> classRoomList(
|
||||
@NotNull(message = "班级ID不能为空") @PathVariable Long classId,
|
||||
XgDormRoomBo bo,
|
||||
PageQuery pageQuery) {
|
||||
return xgDormRoomService.queryPageListByClassId(bo, pageQuery, classId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出宿舍房间信息列表
|
||||
*/
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package org.dromara.xg.domain.bo;
|
||||
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 班主任选择性分配床位请求参数
|
||||
*
|
||||
* @author zlwu
|
||||
* @date 2025-01-27
|
||||
*/
|
||||
@Data
|
||||
public class XgDormSelectiveAllocationBo {
|
||||
|
||||
/**
|
||||
* 选择的床位ID数组
|
||||
*/
|
||||
@NotEmpty(message = "床位ID列表不能为空")
|
||||
private List<Long> bedIds;
|
||||
|
||||
/**
|
||||
* 选择的学生ID数组
|
||||
*/
|
||||
@NotEmpty(message = "学生ID列表不能为空")
|
||||
private List<Long> studentIds;
|
||||
|
||||
/**
|
||||
* 分配类型:random-随机分配,studentNo-按学号分配
|
||||
*/
|
||||
@NotBlank(message = "分配类型不能为空")
|
||||
private String allocationType;
|
||||
}
|
||||
+59
@@ -188,4 +188,63 @@ public interface XgDormBedMapper extends BaseMapperPlus<XgDormBed, XgDormBedVo>
|
||||
List<XgDormBedDetailVo> selectBedDetailsByRoomIdAndCollegeId(@Param("roomId") Long roomId,
|
||||
@Param("collegeId") Long collegeId);
|
||||
|
||||
/**
|
||||
* 查询指定房间中属于指定班级的床位详细信息列表(包含学生信息)
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param classId 班级ID
|
||||
* @return 床位详细信息列表
|
||||
*/
|
||||
@Select("""
|
||||
SELECT
|
||||
b.id,
|
||||
b.bed_code,
|
||||
b.bed_name,
|
||||
b.room_id,
|
||||
r.room_name,
|
||||
b.bed_type,
|
||||
b.bed_status,
|
||||
b.remark as bed_remark,
|
||||
b.create_by,
|
||||
b.create_time,
|
||||
b.update_by,
|
||||
b.update_time,
|
||||
|
||||
bs.id as assignment_id,
|
||||
bs.assign_time,
|
||||
bs.status as assignment_status,
|
||||
|
||||
bs.college_id,
|
||||
college_dept.dept_name as college_name,
|
||||
|
||||
bs.major_id,
|
||||
major_dept.dept_name as major_name,
|
||||
|
||||
bs.class_id,
|
||||
class_dept.dept_name as class_name,
|
||||
c.class_code,
|
||||
c.grade,
|
||||
|
||||
bs.student_id,
|
||||
s.student_no,
|
||||
s.name as student_name,
|
||||
s.gender as student_gender,
|
||||
s.phone as student_phone,
|
||||
s.id_card as student_id_card
|
||||
FROM xg_dorm_bed b
|
||||
LEFT JOIN xg_dorm_room r ON b.room_id = r.id
|
||||
INNER JOIN xg_dorm_bed_student bs ON b.id = bs.bed_id AND bs.status = '0' AND bs.deleted = '0'
|
||||
LEFT JOIN sys_dept college_dept ON bs.college_id = college_dept.dept_id AND college_dept.del_flag = '0'
|
||||
LEFT JOIN sys_dept major_dept ON bs.major_id = major_dept.dept_id AND major_dept.del_flag = '0'
|
||||
LEFT JOIN sys_dept class_dept ON bs.class_id = class_dept.dept_id AND class_dept.del_flag = '0'
|
||||
LEFT JOIN xg_class_msg c ON bs.class_id = c.id AND c.deleted = 0
|
||||
LEFT JOIN xg_student_msg s ON bs.student_id = s.id AND s.deleted = 0
|
||||
WHERE b.room_id = #{roomId}
|
||||
AND bs.class_id = #{classId}
|
||||
AND b.deleted = '0'
|
||||
ORDER BY b.bed_code
|
||||
""")
|
||||
List<XgDormBedDetailVo> selectBedDetailsByRoomIdAndClassId(@Param("roomId") Long roomId,
|
||||
@Param("classId") Long classId);
|
||||
|
||||
}
|
||||
+9
@@ -90,4 +90,13 @@ public interface IXgDormBedService {
|
||||
* @return 床位详细信息列表
|
||||
*/
|
||||
List<XgDormBedDetailVo> queryBedDetailsByBuildingId(Long buildingId);
|
||||
|
||||
/**
|
||||
* 查询指定房间中属于指定班级的床位详细信息列表(包含学生信息)
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param classId 班级ID
|
||||
* @return 床位详细信息列表
|
||||
*/
|
||||
List<XgDormBedDetailVo> queryBedDetailsByRoomIdAndClassId(Long roomId, Long classId);
|
||||
}
|
||||
+2
-1
@@ -12,6 +12,7 @@ import java.util.List;
|
||||
|
||||
import org.dromara.xg.domain.bo.XgDormBedBatchRecallBo;
|
||||
import org.dromara.xg.domain.vo.DormBuildingTreeVo;
|
||||
import org.dromara.xg.domain.bo.XgDormSelectiveAllocationBo;
|
||||
|
||||
/**
|
||||
* 床位学生分配关系Service接口
|
||||
@@ -163,7 +164,7 @@ public interface IXgDormBedStudentService {
|
||||
* @param classId 班级ID,可为空
|
||||
* @return 是否分配成功
|
||||
*/
|
||||
Boolean classTeacherSelectiveAllocation(Object allocationParams, Long classId);
|
||||
Boolean classTeacherSelectiveAllocation(XgDormSelectiveAllocationBo allocationParams, Long classId);
|
||||
|
||||
/**
|
||||
* 班主任一键收回床位
|
||||
|
||||
+10
@@ -42,6 +42,16 @@ public interface IXgDormRoomService {
|
||||
*/
|
||||
TableDataInfo<XgDormRoomVo> queryPageListByCurrentUserCollege(XgDormRoomBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 分页查询指定班级的宿舍房间信息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @param classId 班级ID
|
||||
* @return 宿舍房间信息分页列表
|
||||
*/
|
||||
TableDataInfo<XgDormRoomVo> queryPageListByClassId(XgDormRoomBo bo, PageQuery pageQuery, Long classId);
|
||||
|
||||
/**
|
||||
* 查询符合条件的宿舍房间信息列表
|
||||
*
|
||||
|
||||
+12
@@ -191,6 +191,18 @@ public class XgDormBedServiceImpl implements IXgDormBedService {
|
||||
return baseMapper.selectBedDetailsByBuildingId(buildingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定房间中属于指定班级的床位详细信息列表(包含学生信息)
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param classId 班级ID
|
||||
* @return 床位详细信息列表
|
||||
*/
|
||||
@Override
|
||||
public List<XgDormBedDetailVo> queryBedDetailsByRoomIdAndClassId(Long roomId, Long classId) {
|
||||
return baseMapper.selectBedDetailsByRoomIdAndClassId(roomId, classId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的二级学院ID
|
||||
* 通过当前用户的部门id,向上寻找包括他自己,找到dept_category前几位是xy-的就是二级学院
|
||||
|
||||
+461
-6
@@ -30,8 +30,12 @@ import java.util.Set;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.dromara.xg.domain.bo.XgDormBedBatchRecallBo;
|
||||
import org.dromara.xg.domain.bo.XgDormSelectiveAllocationBo;
|
||||
import org.dromara.xg.mapper.XgStudentMsgMapper;
|
||||
import org.dromara.xg.domain.XgStudentMsg;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
@@ -48,6 +52,8 @@ import java.util.LinkedHashMap;
|
||||
import org.dromara.xg.domain.bo.XgDormBedBatchAllocateToClassBo;
|
||||
import org.dromara.xg.mapper.XgClassMsgMapper;
|
||||
import org.dromara.xg.domain.XgClassMsg;
|
||||
import org.dromara.xg.domain.vo.XgDormBedDetailVo;
|
||||
import org.dromara.xg.domain.bo.XgDormSelectiveAllocationBo;
|
||||
|
||||
/**
|
||||
* 床位学生分配关系Service业务层处理
|
||||
@@ -1188,8 +1194,70 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
|
||||
public List<Object> getClassTeacherBedDetails(Long buildingId, Long classId) {
|
||||
try {
|
||||
Long targetClassId = classId != null ? classId : getCurrentUserClassId();
|
||||
if (targetClassId == null) {
|
||||
log.warn("无法获取班级信息,请确认用户是否为班主任或传入正确的班级ID");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
log.info("获取班主任管理班级的房间床位详情,宿舍楼ID: {},classId:{}", buildingId, targetClassId);
|
||||
return new ArrayList<>();
|
||||
|
||||
// 1. 构建房间查询条件
|
||||
LambdaQueryWrapper<XgDormRoom> roomQuery = Wrappers.lambdaQuery();
|
||||
roomQuery.eq(XgDormRoom::getDeleted, "0")
|
||||
.eq(XgDormRoom::getRoomStatus, "0");
|
||||
|
||||
if (buildingId != null) {
|
||||
roomQuery.eq(XgDormRoom::getBuildingId, buildingId);
|
||||
}
|
||||
|
||||
// 2. 获取该班级分配到的房间ID列表
|
||||
LambdaQueryWrapper<XgDormBedStudent> bedStudentQuery = Wrappers.lambdaQuery();
|
||||
bedStudentQuery.select(XgDormBedStudent::getRoomId)
|
||||
.eq(XgDormBedStudent::getClassId, targetClassId)
|
||||
.eq(XgDormBedStudent::getStatus, "0")
|
||||
.eq(XgDormBedStudent::getDeleted, "0")
|
||||
.isNotNull(XgDormBedStudent::getRoomId)
|
||||
.groupBy(XgDormBedStudent::getRoomId);
|
||||
|
||||
List<XgDormBedStudent> roomRecords = baseMapper.selectList(bedStudentQuery);
|
||||
Set<Long> allocatedRoomIds = roomRecords.stream()
|
||||
.map(XgDormBedStudent::getRoomId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (allocatedRoomIds.isEmpty()) {
|
||||
log.info("班级 {} 没有分配的房间", targetClassId);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 3. 查询这些房间的信息
|
||||
roomQuery.in(XgDormRoom::getId, allocatedRoomIds);
|
||||
List<XgDormRoom> rooms = xgDormRoomMapper.selectList(roomQuery);
|
||||
|
||||
// 4. 为每个房间构建详细信息(房间+床位)
|
||||
List<Object> result = new ArrayList<>();
|
||||
for (XgDormRoom room : rooms) {
|
||||
// 查询该房间中属于指定班级的床位详细信息
|
||||
List<XgDormBedDetailVo> bedDetails = xgDormBedMapper.selectBedDetailsByRoomIdAndClassId(room.getId(),
|
||||
targetClassId);
|
||||
|
||||
// 构建房间记录
|
||||
Map<String, Object> roomRecord = new HashMap<>();
|
||||
roomRecord.put("id", room.getId());
|
||||
roomRecord.put("roomName", room.getRoomName());
|
||||
roomRecord.put("buildingId", room.getBuildingId());
|
||||
roomRecord.put("floorNum", room.getFloorNum());
|
||||
roomRecord.put("roomType", room.getRoomType());
|
||||
roomRecord.put("roomStatus", room.getRoomStatus());
|
||||
roomRecord.put("remark", room.getRemark());
|
||||
roomRecord.put("isBed", false);
|
||||
roomRecord.put("children", bedDetails);
|
||||
|
||||
result.add(roomRecord);
|
||||
}
|
||||
|
||||
log.info("成功获取班级 {} 的房间床位详情,共 {} 个房间", targetClassId, result.size());
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取班主任管理班级的房间床位详情失败", e);
|
||||
return new ArrayList<>();
|
||||
@@ -1199,16 +1267,80 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
|
||||
/**
|
||||
* 获取班主任管理班级的未分配学生列表
|
||||
*
|
||||
* @param classId 班级ID,为空时使用当前用户管理的班级
|
||||
* @return 未分配学生列表
|
||||
*/
|
||||
@Override
|
||||
public List<Object> getClassTeacherUnallocatedStudents(Long classId) {
|
||||
try {
|
||||
Long targetClassId = classId != null ? classId : getCurrentUserClassId();
|
||||
log.info("获取班主任管理班级的未分配学生列表,classId:{}", targetClassId);
|
||||
return new ArrayList<>();
|
||||
if (targetClassId == null) {
|
||||
log.warn("无法获取班级信息,请确认用户是否为班主任或传入正确的班级ID");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
log.info("获取班主任管理班级的未分配学生列表,classId: {}", targetClassId);
|
||||
|
||||
// 1. 查询该班级所有需要住校的学生
|
||||
LambdaQueryWrapper<XgStudentMsg> studentQuery = Wrappers.lambdaQuery();
|
||||
studentQuery.eq(XgStudentMsg::getStudentStatus, "1") // 学生状态为正常
|
||||
.eq(XgStudentMsg::getDeleted, 0L) // 未删除
|
||||
.eq(XgStudentMsg::getIsBoarder, "1") // 需要住校
|
||||
.eq(XgStudentMsg::getClassId, targetClassId) // 指定班级
|
||||
.orderByAsc(XgStudentMsg::getStudentNo); // 按学号排序
|
||||
|
||||
List<XgStudentMsg> boardingStudents = xgStudentMsgMapper.selectList(studentQuery);
|
||||
|
||||
if (boardingStudents.isEmpty()) {
|
||||
log.info("班级 {} 没有需要住校的学生", targetClassId);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
log.info("班级 {} 共有 {} 名学生需要住校", targetClassId, boardingStudents.size());
|
||||
|
||||
// 2. 查询已经分配床位的学生ID列表
|
||||
LambdaQueryWrapper<XgDormBedStudent> bedQuery = Wrappers.lambdaQuery();
|
||||
bedQuery.select(XgDormBedStudent::getStudentId)
|
||||
.eq(XgDormBedStudent::getClassId, targetClassId) // 指定班级
|
||||
.eq(XgDormBedStudent::getStatus, "0") // 正常状态(未退宿)
|
||||
.eq(XgDormBedStudent::getDeleted, "0") // 未删除
|
||||
.isNotNull(XgDormBedStudent::getStudentId); // 学生ID不为空
|
||||
|
||||
List<XgDormBedStudent> allocatedBedRecords = baseMapper.selectList(bedQuery);
|
||||
Set<Long> allocatedStudentIds = allocatedBedRecords.stream()
|
||||
.map(XgDormBedStudent::getStudentId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
log.info("班级 {} 已分配床位的学生数量: {}", targetClassId, allocatedStudentIds.size());
|
||||
|
||||
// 3. 计算未分配床位的学生(需要住校但未分配床位的学生)
|
||||
List<XgStudentMsg> unallocatedStudents = boardingStudents.stream()
|
||||
.filter(student -> !allocatedStudentIds.contains(student.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info("班级 {} 统计结果 - 总住校学生: {}人,已分配床位: {}人,未分配床位: {}人",
|
||||
targetClassId, boardingStudents.size(), allocatedStudentIds.size(), unallocatedStudents.size());
|
||||
|
||||
// 4. 转换为合适的返回格式,包含学生基本信息
|
||||
return unallocatedStudents.stream()
|
||||
.map(student -> {
|
||||
Map<String, Object> studentInfo = new HashMap<>();
|
||||
studentInfo.put("id", student.getId());
|
||||
studentInfo.put("studentNo", student.getStudentNo());
|
||||
studentInfo.put("studentName", student.getName());
|
||||
studentInfo.put("gender", student.getGender());
|
||||
studentInfo.put("classId", student.getClassId());
|
||||
studentInfo.put("majorId", student.getMajorId());
|
||||
studentInfo.put("collegeId", student.getCollegeId());
|
||||
studentInfo.put("phone", student.getPhone());
|
||||
studentInfo.put("idCard", student.getIdCard());
|
||||
return studentInfo;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取班主任管理班级的未分配学生列表失败", e);
|
||||
log.error("获取班主任管理班级的未分配学生列表失败,classId: {}", classId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -1261,18 +1393,341 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean classTeacherSelectiveAllocation(Object allocationParams, Long classId) {
|
||||
public Boolean classTeacherSelectiveAllocation(XgDormSelectiveAllocationBo allocationParams, Long classId) {
|
||||
try {
|
||||
Long targetClassId = classId != null ? classId : getCurrentUserClassId();
|
||||
// TODO: 实现班主任选择性分配床位
|
||||
log.info("班主任选择性分配床位,classId:{},分配参数: {}", targetClassId, allocationParams);
|
||||
|
||||
List<Long> bedIds = allocationParams.getBedIds();
|
||||
List<Long> studentIds = allocationParams.getStudentIds();
|
||||
String allocationType = allocationParams.getAllocationType();
|
||||
|
||||
// 参数校验
|
||||
if (bedIds == null || bedIds.isEmpty()) {
|
||||
throw new RuntimeException("床位ID列表不能为空");
|
||||
}
|
||||
if (studentIds == null || studentIds.isEmpty()) {
|
||||
throw new RuntimeException("学生ID列表不能为空");
|
||||
}
|
||||
if (bedIds.size() < studentIds.size()) {
|
||||
throw new RuntimeException("床位数量不能少于学生数量");
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
Long loginUserId = LoginHelper.getUserId();
|
||||
|
||||
// 1. 验证床位状态 - 确保所有床位都是空闲且有效的
|
||||
LambdaQueryWrapper<XgDormBed> bedQuery = Wrappers.lambdaQuery();
|
||||
bedQuery.in(XgDormBed::getId, bedIds)
|
||||
.eq(XgDormBed::getDeleted, "0");
|
||||
List<XgDormBed> availableBeds = xgDormBedMapper.selectList(bedQuery);
|
||||
|
||||
if (availableBeds.size() != bedIds.size()) {
|
||||
throw new RuntimeException("部分床位不存在或已被删除");
|
||||
}
|
||||
|
||||
// 检查床位是否已被学生占用(床位已分配给学院/班级,但student_id有值表示被学生占用)
|
||||
LambdaQueryWrapper<XgDormBedStudent> existingQuery = Wrappers.lambdaQuery();
|
||||
existingQuery.in(XgDormBedStudent::getBedId, bedIds)
|
||||
.eq(XgDormBedStudent::getStatus, "0")
|
||||
.eq(XgDormBedStudent::getDeleted, "0")
|
||||
.isNotNull(XgDormBedStudent::getStudentId); // 检查是否已有学生占用
|
||||
List<XgDormBedStudent> existingAllocations = baseMapper.selectList(existingQuery);
|
||||
|
||||
if (!existingAllocations.isEmpty()) {
|
||||
// 找出已被占用的床位名称
|
||||
Set<Long> occupiedBedIds = existingAllocations.stream()
|
||||
.map(XgDormBedStudent::getBedId)
|
||||
.collect(Collectors.toSet());
|
||||
List<String> occupiedBedNames = availableBeds.stream()
|
||||
.filter(bed -> occupiedBedIds.contains(bed.getId()))
|
||||
.map(XgDormBed::getBedName)
|
||||
.collect(Collectors.toList());
|
||||
throw new RuntimeException("部分床位已被学生占用:" + String.join("、", occupiedBedNames) + ",请刷新后重试");
|
||||
}
|
||||
|
||||
// 2. 验证学生状态 - 确保所有学生都未分配床位
|
||||
LambdaQueryWrapper<XgStudentMsg> studentQuery = Wrappers.lambdaQuery();
|
||||
studentQuery.in(XgStudentMsg::getId, studentIds)
|
||||
.eq(XgStudentMsg::getDeleted, 0L);
|
||||
List<XgStudentMsg> students = xgStudentMsgMapper.selectList(studentQuery);
|
||||
|
||||
if (students.size() != studentIds.size()) {
|
||||
throw new RuntimeException("部分学生不存在或已被删除");
|
||||
}
|
||||
|
||||
// 检查学生是否已分配床位(通过dormitory_building、dormitory_room、ext_field1三个字段判断)
|
||||
List<XgStudentMsg> alreadyAllocatedStudents = students.stream()
|
||||
.filter(student -> (student.getDormitoryBuilding() != null
|
||||
&& !student.getDormitoryBuilding().trim().isEmpty()) ||
|
||||
(student.getDormitoryRoom() != null && !student.getDormitoryRoom().trim().isEmpty()) ||
|
||||
(student.getExtField1() != null && !student.getExtField1().trim().isEmpty()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!alreadyAllocatedStudents.isEmpty()) {
|
||||
String allocatedStudentNames = alreadyAllocatedStudents.stream()
|
||||
.map(student -> student.getName() + "(" + student.getStudentNo() + ")")
|
||||
.collect(Collectors.joining("、"));
|
||||
throw new RuntimeException("学生 " + allocatedStudentNames + " 已分配宿舍,请刷新后重试");
|
||||
}
|
||||
|
||||
// 3. 根据分配类型进行床位分配
|
||||
List<Long> finalBedIds;
|
||||
List<Long> finalStudentIds;
|
||||
|
||||
if ("random".equals(allocationType)) {
|
||||
// 随机分配:打乱床位顺序
|
||||
finalBedIds = new ArrayList<>(bedIds);
|
||||
Collections.shuffle(finalBedIds);
|
||||
finalStudentIds = new ArrayList<>(studentIds);
|
||||
} else if ("studentNo".equals(allocationType)) {
|
||||
// 按学号分配:学生按学号排序,床位按名称排序
|
||||
Map<Long, String> studentNoMap = students.stream()
|
||||
.collect(Collectors.toMap(XgStudentMsg::getId, XgStudentMsg::getStudentNo));
|
||||
|
||||
finalStudentIds = studentIds.stream()
|
||||
.sorted((id1, id2) -> {
|
||||
String no1 = studentNoMap.get(id1);
|
||||
String no2 = studentNoMap.get(id2);
|
||||
return no1.compareToIgnoreCase(no2);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 创建床位ID到床位信息的映射
|
||||
Map<Long, XgDormBed> bedInfoMap = availableBeds.stream()
|
||||
.collect(Collectors.toMap(XgDormBed::getId, bed -> bed));
|
||||
|
||||
// 创建房间ID到房间名称的映射
|
||||
Set<Long> roomIds = availableBeds.stream()
|
||||
.map(XgDormBed::getRoomId)
|
||||
.collect(Collectors.toSet());
|
||||
LambdaQueryWrapper<XgDormRoom> roomQuery = Wrappers.lambdaQuery();
|
||||
roomQuery.in(XgDormRoom::getId, roomIds)
|
||||
.eq(XgDormRoom::getDeleted, "0");
|
||||
List<XgDormRoom> rooms = xgDormRoomMapper.selectList(roomQuery);
|
||||
Map<Long, String> roomNameMap = rooms.stream()
|
||||
.collect(Collectors.toMap(XgDormRoom::getId, XgDormRoom::getRoomName));
|
||||
|
||||
finalBedIds = bedIds.stream()
|
||||
.sorted((id1, id2) -> {
|
||||
XgDormBed bed1 = bedInfoMap.get(id1);
|
||||
XgDormBed bed2 = bedInfoMap.get(id2);
|
||||
String roomName1 = roomNameMap.get(bed1.getRoomId());
|
||||
String roomName2 = roomNameMap.get(bed2.getRoomId());
|
||||
|
||||
// 先按房间名称排序
|
||||
int roomCompare = roomName1.compareToIgnoreCase(roomName2);
|
||||
if (roomCompare != 0) {
|
||||
return roomCompare;
|
||||
}
|
||||
// 房间相同时,再按床位名称排序
|
||||
return bed1.getBedName().compareToIgnoreCase(bed2.getBedName());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
throw new RuntimeException("不支持的分配类型:" + allocationType);
|
||||
}
|
||||
|
||||
// 创建床位-学生映射关系
|
||||
Map<Long, Long> bedStudentMap = new HashMap<>();
|
||||
for (int i = 0; i < finalStudentIds.size(); i++) {
|
||||
bedStudentMap.put(finalBedIds.get(i), finalStudentIds.get(i));
|
||||
}
|
||||
|
||||
// 4. 更新或插入xg_dorm_bed_student表
|
||||
Map<Long, XgDormBed> bedMap = availableBeds.stream()
|
||||
.collect(Collectors.toMap(XgDormBed::getId, bed -> bed));
|
||||
Map<Long, XgStudentMsg> studentMap = students.stream()
|
||||
.collect(Collectors.toMap(XgStudentMsg::getId, student -> student));
|
||||
|
||||
// 查询现有的床位分配记录(已分配给班级但没有学生的记录)
|
||||
LambdaQueryWrapper<XgDormBedStudent> existingBedQuery = Wrappers.lambdaQuery();
|
||||
existingBedQuery.in(XgDormBedStudent::getBedId, bedIds)
|
||||
.eq(XgDormBedStudent::getClassId, targetClassId)
|
||||
.eq(XgDormBedStudent::getStatus, "0")
|
||||
.eq(XgDormBedStudent::getDeleted, "0")
|
||||
.isNull(XgDormBedStudent::getStudentId); // 没有学生的记录
|
||||
List<XgDormBedStudent> existingBedRecords = baseMapper.selectList(existingBedQuery);
|
||||
|
||||
// 创建床位ID到现有记录的映射
|
||||
Map<Long, XgDormBedStudent> existingBedMap = existingBedRecords.stream()
|
||||
.collect(Collectors.toMap(XgDormBedStudent::getBedId, record -> record));
|
||||
|
||||
List<XgDormBedStudent> bedStudentAllocations = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<Long, Long> entry : bedStudentMap.entrySet()) {
|
||||
Long bedId = entry.getKey();
|
||||
Long studentId = entry.getValue();
|
||||
XgDormBed bed = bedMap.get(bedId);
|
||||
XgStudentMsg student = studentMap.get(studentId);
|
||||
|
||||
XgDormBedStudent existingRecord = existingBedMap.get(bedId);
|
||||
if (existingRecord != null) {
|
||||
// 更新现有记录:添加学生信息
|
||||
LambdaUpdateWrapper<XgDormBedStudent> updateWrapper = Wrappers.lambdaUpdate();
|
||||
updateWrapper.eq(XgDormBedStudent::getId, existingRecord.getId())
|
||||
.set(XgDormBedStudent::getStudentId, studentId)
|
||||
.set(XgDormBedStudent::getAssignTime, now)
|
||||
.set(XgDormBedStudent::getUpdateBy, loginUserId)
|
||||
.set(XgDormBedStudent::getUpdateTime, now);
|
||||
baseMapper.update(null, updateWrapper);
|
||||
} else {
|
||||
// 创建新的分配记录
|
||||
XgDormBedStudent allocation = new XgDormBedStudent();
|
||||
allocation.setBedId(bedId);
|
||||
allocation.setStudentId(studentId);
|
||||
allocation.setRoomId(bed.getRoomId());
|
||||
allocation.setBuildingId(getBuildingIdByBedId(bedId));
|
||||
allocation.setAreaId(getAreaIdByBedId(bedId));
|
||||
allocation.setCampusId(getCampusIdByBedId(bedId));
|
||||
|
||||
// 从学生信息获取学院、专业、班级信息
|
||||
allocation.setCollegeId(getCollegeIdByClassId(targetClassId));
|
||||
allocation.setMajorId(getMajorIdByClassId(targetClassId));
|
||||
allocation.setClassId(targetClassId);
|
||||
|
||||
allocation.setStatus("0"); // 0正常
|
||||
allocation.setAssignTime(now);
|
||||
allocation.setCreateBy(loginUserId);
|
||||
allocation.setCreateTime(now);
|
||||
allocation.setUpdateBy(loginUserId);
|
||||
allocation.setUpdateTime(now);
|
||||
allocation.setDeleted("0");
|
||||
|
||||
bedStudentAllocations.add(allocation);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量插入新的分配关系
|
||||
if (!bedStudentAllocations.isEmpty()) {
|
||||
baseMapper.insertBatch(bedStudentAllocations);
|
||||
}
|
||||
|
||||
// 5. 批量更新xg_student_msg表
|
||||
for (Map.Entry<Long, Long> entry : bedStudentMap.entrySet()) {
|
||||
Long bedId = entry.getKey();
|
||||
Long studentId = entry.getValue();
|
||||
XgDormBed bed = bedMap.get(bedId);
|
||||
|
||||
LambdaUpdateWrapper<XgStudentMsg> updateStudent = Wrappers.lambdaUpdate();
|
||||
updateStudent.eq(XgStudentMsg::getId, studentId)
|
||||
.eq(XgStudentMsg::getDeleted, 0L)
|
||||
.set(XgStudentMsg::getDormitoryBuilding, getBuildingIdByBedId(bedId).toString())
|
||||
.set(XgStudentMsg::getDormitoryRoom, bed.getRoomId().toString())
|
||||
.set(XgStudentMsg::getExtField1, bedId.toString())
|
||||
.set(XgStudentMsg::getUpdateBy, loginUserId)
|
||||
.set(XgStudentMsg::getUpdateTime, now);
|
||||
|
||||
xgStudentMsgMapper.update(null, updateStudent);
|
||||
}
|
||||
|
||||
// 6. 更新床位状态为已分配
|
||||
LambdaUpdateWrapper<XgDormBed> updateBed = Wrappers.lambdaUpdate();
|
||||
updateBed.in(XgDormBed::getId, bedStudentMap.keySet())
|
||||
.eq(XgDormBed::getDeleted, "0")
|
||||
.set(XgDormBed::getBedStatus, "1") // 1已分配
|
||||
.set(XgDormBed::getUpdateBy, loginUserId)
|
||||
.set(XgDormBed::getUpdateTime, now);
|
||||
|
||||
xgDormBedMapper.update(null, updateBed);
|
||||
|
||||
log.info("班主任选择性分配床位成功,分配了{}个床位给{}名学生", bedStudentMap.size(), finalStudentIds.size());
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("班主任选择性分配床位失败", e);
|
||||
throw new RuntimeException("选择性分配床位失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据床位ID获取楼栋ID
|
||||
*/
|
||||
private Long getBuildingIdByBedId(Long bedId) {
|
||||
// 通过床位ID查询房间ID,再通过房间ID查询楼栋ID
|
||||
LambdaQueryWrapper<XgDormBed> bedQuery = Wrappers.lambdaQuery();
|
||||
bedQuery.eq(XgDormBed::getId, bedId).eq(XgDormBed::getDeleted, "0");
|
||||
XgDormBed bed = xgDormBedMapper.selectOne(bedQuery);
|
||||
|
||||
if (bed != null && bed.getRoomId() != null) {
|
||||
LambdaQueryWrapper<XgDormRoom> roomQuery = Wrappers.lambdaQuery();
|
||||
roomQuery.eq(XgDormRoom::getId, bed.getRoomId()).eq(XgDormRoom::getDeleted, "0");
|
||||
XgDormRoom room = xgDormRoomMapper.selectOne(roomQuery);
|
||||
|
||||
if (room != null) {
|
||||
return room.getBuildingId();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据床位ID获取宿舍区ID
|
||||
*/
|
||||
private Long getAreaIdByBedId(Long bedId) {
|
||||
Long buildingId = getBuildingIdByBedId(bedId);
|
||||
if (buildingId != null) {
|
||||
LambdaQueryWrapper<XgDormBuilding> buildingQuery = Wrappers.lambdaQuery();
|
||||
buildingQuery.eq(XgDormBuilding::getId, buildingId).eq(XgDormBuilding::getDeleted, "0");
|
||||
XgDormBuilding building = xgDormBuildingMapper.selectOne(buildingQuery);
|
||||
|
||||
if (building != null) {
|
||||
return building.getAreaId();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据床位ID获取校区ID
|
||||
*/
|
||||
private Long getCampusIdByBedId(Long bedId) {
|
||||
Long buildingId = getBuildingIdByBedId(bedId);
|
||||
if (buildingId != null) {
|
||||
LambdaQueryWrapper<XgDormBuilding> buildingQuery = Wrappers.lambdaQuery();
|
||||
buildingQuery.eq(XgDormBuilding::getId, buildingId).eq(XgDormBuilding::getDeleted, "0");
|
||||
XgDormBuilding building = xgDormBuildingMapper.selectOne(buildingQuery);
|
||||
|
||||
if (building != null) {
|
||||
return building.getCampusId();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据班级ID获取学院ID
|
||||
*/
|
||||
private Long getCollegeIdByClassId(Long classId) {
|
||||
LambdaQueryWrapper<XgClassMsg> classQuery = Wrappers.lambdaQuery();
|
||||
classQuery.eq(XgClassMsg::getId, classId).eq(XgClassMsg::getDeleted, 0L);
|
||||
XgClassMsg classMsg = xgClassMsgMapper.selectOne(classQuery);
|
||||
|
||||
if (classMsg != null) {
|
||||
return classMsg.getCollegeId();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据班级ID获取专业ID
|
||||
*/
|
||||
private Long getMajorIdByClassId(Long classId) {
|
||||
LambdaQueryWrapper<XgClassMsg> classQuery = Wrappers.lambdaQuery();
|
||||
classQuery.eq(XgClassMsg::getId, classId).eq(XgClassMsg::getDeleted, 0L);
|
||||
XgClassMsg classMsg = xgClassMsgMapper.selectOne(classQuery);
|
||||
|
||||
if (classMsg != null) {
|
||||
return classMsg.getMajorId();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 班主任一键收回床位
|
||||
*
|
||||
|
||||
+55
@@ -98,6 +98,37 @@ public class XgDormRoomServiceImpl implements IXgDormRoomService {
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询指定班级的宿舍房间信息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @param classId 班级ID
|
||||
* @return 宿舍房间信息分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<XgDormRoomVo> queryPageListByClassId(XgDormRoomBo bo, PageQuery pageQuery, Long classId) {
|
||||
// 1. 参数校验
|
||||
if (classId == null) {
|
||||
log.warn("班级ID不能为空,返回空结果");
|
||||
return TableDataInfo.build(new Page<>());
|
||||
}
|
||||
|
||||
// 2. 获取分配给指定班级的房间ID列表
|
||||
Set<Long> classAllocatedRoomIds = getClassAllocatedRoomIds(classId);
|
||||
if (classAllocatedRoomIds.isEmpty()) {
|
||||
log.info("班级 {} 没有分配的房间,返回空结果", classId);
|
||||
return TableDataInfo.build(new Page<>());
|
||||
}
|
||||
|
||||
// 3. 构建查询条件,只查询分配给指定班级的具体房间,并支持额外的过滤条件
|
||||
LambdaQueryWrapper<XgDormRoom> lqw = buildQueryWrapper(bo);
|
||||
lqw.in(XgDormRoom::getId, classAllocatedRoomIds);
|
||||
|
||||
Page<XgDormRoomVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的宿舍房间信息列表
|
||||
*
|
||||
@@ -255,4 +286,28 @@ public class XgDormRoomServiceImpl implements IXgDormRoomService {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分配给指定班级的房间ID列表
|
||||
*/
|
||||
private Set<Long> getClassAllocatedRoomIds(Long classId) {
|
||||
try {
|
||||
LambdaQueryWrapper<XgDormBedStudent> queryWrapper = Wrappers.lambdaQuery();
|
||||
queryWrapper.select(XgDormBedStudent::getRoomId)
|
||||
.eq(XgDormBedStudent::getClassId, classId)
|
||||
.eq(XgDormBedStudent::getStatus, "0")
|
||||
.eq(XgDormBedStudent::getDeleted, "0")
|
||||
.isNotNull(XgDormBedStudent::getRoomId)
|
||||
.groupBy(XgDormBedStudent::getRoomId);
|
||||
|
||||
List<XgDormBedStudent> roomRecords = xgDormBedStudentMapper.selectList(queryWrapper);
|
||||
return roomRecords.stream()
|
||||
.map(XgDormBedStudent::getRoomId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取班级 {} 分配的房间ID列表失败", classId, e);
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,4 +147,32 @@
|
||||
|
||||
**📞 联系方式**:如有问题或建议,请联系开发团队
|
||||
**📅 最后更新**:2025-01-14
|
||||
**🔄 模块状态**:设计阶段
|
||||
**🔄 模块状态**:设计阶段
|
||||
|
||||
## 🆕 前端功能更新(班主任分配床位给学生)
|
||||
|
||||
- 新增页面:`web/src/views/xg/xgss/bzrfp/bzrfp.vue`
|
||||
- 班主任可在“我的班级”Tab 中,针对每个班级进行床位分配给学生
|
||||
- 左侧:宿舍楼树(通过接口获取,仅宿舍楼节点可选)
|
||||
- 右侧:房间/床位树表,支持选择空闲床位与状态查看
|
||||
- 操作:选择性分配(弹窗)、一键随机分配、按学号分配、一键收回
|
||||
- 弹窗组件:`web/src/views/xg/xgss/bzrfp/components/SelectiveAllocationDialog.vue`
|
||||
- 左侧为可用床位列表,右侧为该班级未分配且有住宿需求的学生列表
|
||||
- 支持随机分配/按学号分配,两侧数量需一致
|
||||
|
||||
- 相关前端API(已存在于相应模块,无需新增后端接口):
|
||||
- 获取班主任宿舍楼树:`getClassTeacherDormTree(classId)` → `@/api/xg/xgss/dormBedStudent/dormBedStudent` 目录
|
||||
- 获取房间床位详情(含学生):`getClassTeacherBedDetails(buildingId, classId)`
|
||||
- 获取未分配学生列表:`getClassTeacherUnallocatedStudents(classId)`
|
||||
- 选择性分配:`classTeacherSelectiveAllocation({ bedIds, studentIds, allocationType }, classId)`
|
||||
- 一键随机分配:`classTeacherRandomAllocation(buildingId, classId)`
|
||||
- 按学号分配:`classTeacherStudentNoAllocation(buildingId, classId)`
|
||||
- 一键收回:`classTeacherRecallBeds(buildingId, classId)`
|
||||
- 获取我管理的班级列表:`getMyClassListAsTeacher()` → `@/api/xg/xsjcxx/classMsg/classMsg`
|
||||
|
||||
- 数据来源说明:
|
||||
- 分配数据基于 `xg_dorm_bed_student` 关联 `xg_dorm_room`、`xg_student_msg` 实现:
|
||||
- 通过 `building_id + class_id` 获取该班在楼栋下的房间与床位分配
|
||||
- 通过 `student_id` 判定床位是否已入住,并展示学生信息
|
||||
|
||||
> 本次为前端界面与交互完善,无数据库表结构与后端接口变更。文档同步记录以便后续联调与测试追溯。
|
||||
@@ -94,6 +94,18 @@ export const getBedDetailsByBuildingId = (buildingId: string | number): AxiosPro
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询指定房间中属于指定班级的床位详细信息列表(包含学生信息)
|
||||
* @param roomId 房间ID
|
||||
* @param classId 班级ID
|
||||
*/
|
||||
export const getBedDetailsByRoomIdAndClassId = (roomId: string | number, classId: string | number): AxiosPromise<DormBedDetailVO[]> => {
|
||||
return request({
|
||||
url: `/dormbed/dormBed/details/room/${roomId}/class/${classId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量分配床位给二级学院
|
||||
* @param data 批量分配参数
|
||||
|
||||
@@ -64,6 +64,32 @@ export const updateDormRoom = (data: DormRoomForm) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询指定班级的宿舍房间信息列表
|
||||
* @param classId 班级ID
|
||||
* @param query 查询条件
|
||||
* @returns {*}
|
||||
*/
|
||||
export const listDormRoomByClassId = (classId: string | number, query?: DormRoomQuery): AxiosPromise<DormRoomVO[]> => {
|
||||
return request({
|
||||
url: `/xgss/dormRoom/class/${classId}/list`,
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询指定房间的床位详细信息列表(包含学生信息)
|
||||
* @param roomId 房间ID
|
||||
* @returns {*}
|
||||
*/
|
||||
export const getDormBedDetailsByRoomId = (roomId: string | number): AxiosPromise<any[]> => {
|
||||
return request({
|
||||
url: `/dormbed/dormBed/details/room/${roomId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除宿舍房间信息
|
||||
* @param id
|
||||
|
||||
@@ -0,0 +1,685 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<div class="flex-layout">
|
||||
<!-- 左侧:宿舍树(不含学生树) -->
|
||||
<div class="tree-sidebar">
|
||||
<!-- 班级切换 Tabs -->
|
||||
<el-card shadow="hover" class="tree-card">
|
||||
<template #header>
|
||||
<div class="card-header-title">我的班级</div>
|
||||
</template>
|
||||
<el-tabs v-model="activeClassId" type="card" @tab-click="handleClassTabChange">
|
||||
<el-tab-pane v-for="cls in myClassList" :key="String(cls.id)" :label="`${cls.className}`" :name="String(cls.id)" />
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<!-- 宿舍楼树 -->
|
||||
<el-card shadow="hover" class="tree-card mt-4">
|
||||
<div class="card-header-title">宿舍楼</div>
|
||||
<el-input
|
||||
v-model="buildingName"
|
||||
prefix-icon="Search"
|
||||
placeholder="请输入宿舍楼名称"
|
||||
clearable
|
||||
@clear="watchBuildingName"
|
||||
@keyup.enter="watchBuildingName"
|
||||
/>
|
||||
<el-tree
|
||||
ref="dormTreeRef"
|
||||
class="mt-2"
|
||||
node-key="id"
|
||||
:data="dormOptions"
|
||||
:props="{ label: 'label', children: 'children', disabled: 'disabled' }"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="filterNode"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="custom-tree-node">
|
||||
<el-tooltip v-if="data.disabled" effect="dark" content="此节点不可选择,请选择具体的宿舍楼" placement="right">
|
||||
<span :class="{ 'disabled-node': data.disabled }">{{ node.label }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else>
|
||||
<span v-if="data.type === 'building'" :class="data.buildingType === '1' ? 'male-building' : 'female-building'">
|
||||
{{ data.buildingType === '1' ? '(男)' : '(女)' }}
|
||||
</span>
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="content-area">
|
||||
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" inline>
|
||||
<el-form-item label="房间号" prop="roomName">
|
||||
<el-input v-model="queryParams.roomName" placeholder="请输入房间号" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="roomStatus">
|
||||
<el-select v-model="queryParams.roomStatus" placeholder="房间状态" clearable>
|
||||
<el-option label="正常" value="0" />
|
||||
<el-option label="维修中" value="1" />
|
||||
<el-option label="停用" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card v-if="!activeClassId" shadow="hover" class="empty-tip">
|
||||
<div class="flex flex-col items-center justify-center h-[400px]">
|
||||
<el-icon class="text-gray-400 text-5xl mb-4"><InfoFilled /></el-icon>
|
||||
<p class="text-gray-500 text-lg">当前账号没有班主任班级</p>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-else-if="!selectedBuilding" shadow="hover" class="empty-tip">
|
||||
<div class="flex flex-col items-center justify-center h-[400px]">
|
||||
<el-icon class="text-gray-400 text-5xl mb-4"><InfoFilled /></el-icon>
|
||||
<p class="text-gray-500 text-lg">请在左侧选择宿舍楼</p>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<template v-else>
|
||||
<el-card shadow="hover" class="mb-4">
|
||||
<template #header>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" icon="check" size="small" @click="openSelectiveAllocation">分配给学生</el-button>
|
||||
<el-button type="success" icon="Refresh" size="small" @click="handleRandomAllocation" :loading="actionLoading"
|
||||
>一键随机分配</el-button
|
||||
>
|
||||
<el-button type="primary" icon="Sort" size="small" @click="handleStudentNoAllocation" :loading="actionLoading"
|
||||
>按学号分配</el-button
|
||||
>
|
||||
<el-button type="warning" icon="RefreshLeft" size="small" @click="handleRecall" :loading="actionLoading">一键收回</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right">
|
||||
<el-button link type="primary" size="small" :loading="expandAllLoading" :disabled="expandAllLoading || loading" @click="expandAll"
|
||||
>全部展开</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="collapseAllLoading"
|
||||
:disabled="collapseAllLoading || loading"
|
||||
@click="collapseAll"
|
||||
>全部收起</el-button
|
||||
>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 操作信息 -->
|
||||
<div class="mb-4">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<span class="font-bold whitespace-nowrap"
|
||||
>{{ selectedBuilding?.label }}
|
||||
<span :class="selectedBuilding?.buildingType === '1' ? 'male-building' : 'female-building'">
|
||||
{{ selectedBuilding?.buildingType === '1' ? '(男生楼)' : '(女生楼)' }}
|
||||
</span>
|
||||
- 房间与床位</span
|
||||
>
|
||||
<div class="text-blue-500 flex flex-wrap items-center gap-2">
|
||||
<span class="whitespace-nowrap"> 已选择: {{ selectedBedCount }} 个床位 </span>
|
||||
<span v-if="selectedBedCount > 0" class="whitespace-nowrap text-sm">
|
||||
[可分配: <el-tag size="small" type="success">{{ selectedBedStats.available }}</el-tag> 个, 已分配:
|
||||
<el-tag size="small" type="warning">{{ selectedBedStats.allocated }}</el-tag> 个, 维修中:
|
||||
<el-tag size="small" type="danger">{{ selectedBedStats.maintenance }}</el-tag>
|
||||
个]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 房间及床位树形表格(仅床位可选) -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading || expandAllLoading || collapseAllLoading"
|
||||
element-loading-text="处理中..."
|
||||
:data="filteredRoomList"
|
||||
row-key="id"
|
||||
border
|
||||
:expand-row-keys="expandedRowKeys"
|
||||
@expand-change="handleExpandChange"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
table-layout="auto"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="160">
|
||||
<template #default="scope">
|
||||
<span v-if="!scope.row.isBed">{{ scope.row.roomName }} (房间)</span>
|
||||
<span v-else>{{ scope.row.bedName }} (床位)</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="floorNum" label="楼层" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<span v-if="!scope.row.isBed">{{ scope.row.floorNum }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<template v-if="!scope.row.isBed">
|
||||
<span>{{ scope.row.roomType === '1' ? '标准间' : scope.row.roomType === '2' ? '单人间' : '套间' }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag v-if="scope.row.bedType === '1'" type="success" size="small">下铺</el-tag>
|
||||
<el-tag v-else-if="scope.row.bedType === '2'" type="warning" size="small">上铺</el-tag>
|
||||
<el-tag v-else-if="scope.row.bedType === '3'" type="danger" size="small">中铺</el-tag>
|
||||
<el-tag v-else type="info" size="small">独立床</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<template v-if="!scope.row.isBed">
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<el-tag v-if="scope.row.roomStatus === '0'" type="success">正常</el-tag>
|
||||
<el-tag v-else-if="scope.row.roomStatus === '1'" type="warning">维修中</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
<span v-if="scope.row.roomStatus === '0'" class="text-xs text-gray-500">
|
||||
(已分配: {{ scope.row.children?.filter((bed) => bed.studentId).length || 0 }}/{{ scope.row.children?.length || 0 }}床位)
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag v-if="scope.row.bedStatus === '2'" type="danger">维修中</el-tag>
|
||||
<el-tag v-else-if="scope.row.studentId" type="warning">已分配</el-tag>
|
||||
<el-tag v-else type="success">空闲</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学号" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.isBed && scope.row.studentNo">{{ scope.row.studentNo }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学生姓名" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.isBed && scope.row.studentName">{{ scope.row.studentName }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" width="60" align="center">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.isBed && scope.row.studentGender !== undefined">
|
||||
<el-tag v-if="scope.row.studentGender === '0'" type="primary" size="small">男</el-tag>
|
||||
<el-tag v-else-if="scope.row.studentGender === '1'" class="female-tag" size="small">女</el-tag>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="150" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
{{ scope.row.remark || scope.row.bedRemark }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择性分配对话框(床位 ←→ 学生) -->
|
||||
<selective-allocation-dialog
|
||||
v-model="selectiveDialog.visible"
|
||||
:title="selectiveDialog.title"
|
||||
:building-info="selectedBuilding"
|
||||
:class-id="activeClassId"
|
||||
@success="handleAllocationSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ClassTeacherBedAllocation">
|
||||
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import type { DormBuildingTreeVO } from '@/api/xg/xgss/dormBedStudent/dormBedStudent/types';
|
||||
import {
|
||||
getClassTeacherDormTree,
|
||||
classTeacherRandomAllocation,
|
||||
classTeacherStudentNoAllocation,
|
||||
classTeacherRecallBeds
|
||||
} from '@/api/xg/xgss/dormBedStudent/dormBedStudent';
|
||||
import { listDormRoomByClassId, getDormBedDetailsByRoomId } from '@/api/xg/xgss/dormRoom';
|
||||
import type { ClassMsgVO } from '@/api/xg/xsjcxx/classMsg/types';
|
||||
import { getMyClassListAsTeacher } from '@/api/xg/xsjcxx/classMsg/classMsg';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import ElMessageBox from 'element-plus/es/components/message-box/index';
|
||||
|
||||
// 组件
|
||||
import SelectiveAllocationDialog from './components/SelectiveAllocationDialog.vue';
|
||||
|
||||
// 左侧树&查询
|
||||
const dormTreeRef = ref();
|
||||
const queryFormRef = ref();
|
||||
const tableRef = ref();
|
||||
const buildingName = ref('');
|
||||
const showSearch = ref(true);
|
||||
const loading = ref(false);
|
||||
const expandAllLoading = ref(false);
|
||||
const collapseAllLoading = ref(false);
|
||||
const actionLoading = ref(false);
|
||||
const expandedRowKeys = ref<string[]>([]);
|
||||
|
||||
const animate = reactive({
|
||||
searchAnimate: {
|
||||
enter: 'animate__animated animate__fadeInDown',
|
||||
leave: 'animate__animated animate__fadeOutUp'
|
||||
}
|
||||
});
|
||||
|
||||
// Tabs 班级
|
||||
const myClassList = ref<ClassMsgVO[]>([]);
|
||||
const activeClassId = ref<string>('');
|
||||
|
||||
// 宿舍树数据
|
||||
const dormOptions = ref<DormBuildingTreeVO[]>([]);
|
||||
const selectedBuilding = ref<DormBuildingTreeVO>();
|
||||
|
||||
// 房间/床位
|
||||
const roomList = ref<any[]>([]);
|
||||
|
||||
// 查询参数(前端过滤)
|
||||
const queryParams = reactive({
|
||||
roomName: '',
|
||||
roomStatus: undefined as string | undefined
|
||||
});
|
||||
|
||||
// 选择统计
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const selectedBedCount = ref(0);
|
||||
const selectedBedStats = ref({
|
||||
available: 0,
|
||||
allocated: 0,
|
||||
maintenance: 0
|
||||
});
|
||||
|
||||
/** 通过条件过滤节点 */
|
||||
const filterNode = (value: string, data: DormBuildingTreeVO) => {
|
||||
if (!value) return true;
|
||||
return (data.label || '').toLowerCase().indexOf(value.toLowerCase()) !== -1;
|
||||
};
|
||||
|
||||
/** 监听宿舍楼名称变化,筛选树 */
|
||||
const watchBuildingName = () => {
|
||||
dormTreeRef.value?.filter(buildingName.value);
|
||||
};
|
||||
watch(buildingName, watchBuildingName);
|
||||
|
||||
/** 班级切换 */
|
||||
const handleClassTabChange = async () => {
|
||||
selectedBuilding.value = undefined;
|
||||
roomList.value = [];
|
||||
await loadDormTree();
|
||||
};
|
||||
|
||||
/** 获取我的班级(班主任) */
|
||||
const loadMyClassList = async () => {
|
||||
try {
|
||||
const res = await getMyClassListAsTeacher();
|
||||
myClassList.value = (res.data || []) as any;
|
||||
if (myClassList.value.length > 0) {
|
||||
activeClassId.value = String(myClassList.value[0].id);
|
||||
await loadDormTree();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取班主任宿舍楼树 */
|
||||
const loadDormTree = async () => {
|
||||
try {
|
||||
const res = await getClassTeacherDormTree(activeClassId.value);
|
||||
dormOptions.value = (res.data || []).map((node: any) => ({
|
||||
...node,
|
||||
disabled: node.type !== 'building'
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('获取宿舍楼树失败', e);
|
||||
}
|
||||
};
|
||||
|
||||
/** 节点点击:仅 building 有效 */
|
||||
const handleNodeClick = (data: any) => {
|
||||
if (data.type !== 'building') return;
|
||||
selectedBuilding.value = data;
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 查询(重新加载房间床位数据) */
|
||||
const handleQuery = () => {
|
||||
loadRoomData();
|
||||
};
|
||||
|
||||
/** 重置查询 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields?.();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 加载房间与床位(使用新的班级房间查询接口) */
|
||||
const loadRoomData = async () => {
|
||||
if (!selectedBuilding.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
// 1. 调用新的班级房间查询接口,获取指定班级在指定宿舍楼的房间信息
|
||||
const roomRes = await listDormRoomByClassId(activeClassId.value as any, {
|
||||
buildingId: selectedBuilding.value.id as any,
|
||||
pageNum: 1,
|
||||
pageSize: 1000 // 设置较大的页面大小获取所有数据
|
||||
});
|
||||
|
||||
// 后端返回的是TableDataInfo格式,从rows字段获取实际数据
|
||||
const roomData = (roomRes.data as any)?.rows || roomRes.data || [];
|
||||
|
||||
// 2. 为每个房间获取床位详细信息
|
||||
const roomsWithBeds = await Promise.all(
|
||||
roomData.map(async (room: any) => {
|
||||
try {
|
||||
const bedRes = await getDormBedDetailsByRoomId(room.id);
|
||||
const bedData = bedRes.data || [];
|
||||
return {
|
||||
...room,
|
||||
isBed: false,
|
||||
children: bedData.map((bed: any) => ({
|
||||
...bed,
|
||||
isBed: true
|
||||
}))
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn(`获取房间 ${room.id} 的床位信息失败:`, e);
|
||||
return {
|
||||
...room,
|
||||
isBed: false,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
roomList.value = roomsWithBeds;
|
||||
// 默认清空选择
|
||||
nextTick(() => tableRef.value?.clearSelection());
|
||||
selectedRows.value = [];
|
||||
updateSelectionCount([]);
|
||||
} catch (e) {
|
||||
console.error('加载房间床位失败', e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 过滤后的房间数据(前端过滤) */
|
||||
const filteredRoomList = computed(() => {
|
||||
const name = (queryParams.roomName || '').trim().toLowerCase();
|
||||
const status = queryParams.roomStatus;
|
||||
if (!name && !status) return roomList.value;
|
||||
const matchRoom = (room: any) => {
|
||||
const nameOk = !name || (room.roomName || '').toLowerCase().includes(name);
|
||||
const statusOk = !status || room.roomStatus === status;
|
||||
return nameOk && statusOk;
|
||||
};
|
||||
return roomList.value
|
||||
.filter((room) => matchRoom(room))
|
||||
.map((room) => ({
|
||||
...room,
|
||||
children: room.children || []
|
||||
}));
|
||||
});
|
||||
|
||||
/** 展开/收起 */
|
||||
const handleExpandChange = (row: any, expanded: boolean) => {
|
||||
const key = String(row.id);
|
||||
if (expanded) expandedRowKeys.value.push(key);
|
||||
else expandedRowKeys.value = expandedRowKeys.value.filter((k) => k !== key);
|
||||
};
|
||||
const expandAll = async () => {
|
||||
if (expandAllLoading.value || !filteredRoomList.value?.length) return;
|
||||
expandAllLoading.value = true;
|
||||
await nextTick();
|
||||
expandedRowKeys.value = filteredRoomList.value.map((room) => String(room.id));
|
||||
await nextTick();
|
||||
expandAllLoading.value = false;
|
||||
};
|
||||
const collapseAll = async () => {
|
||||
if (collapseAllLoading.value) return;
|
||||
collapseAllLoading.value = true;
|
||||
await nextTick();
|
||||
expandedRowKeys.value = [];
|
||||
await nextTick();
|
||||
collapseAllLoading.value = false;
|
||||
};
|
||||
|
||||
/** 行可选(仅未分配且非维修中的床位可选) */
|
||||
const checkSelectable = (row: any) => {
|
||||
return row.isBed && !row.studentId && row.bedStatus !== '2';
|
||||
};
|
||||
|
||||
/** 选择变化 */
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectedRows.value = rows;
|
||||
updateSelectionCount(rows);
|
||||
};
|
||||
|
||||
/** 更新选择统计 */
|
||||
const updateSelectionCount = (rows: any[]) => {
|
||||
const bedStats = { available: 0, allocated: 0, maintenance: 0 } as any;
|
||||
rows.forEach((row) => {
|
||||
if (!row.isBed) return;
|
||||
if (row.bedStatus === '2') bedStats.maintenance++;
|
||||
else if (row.studentId) bedStats.allocated++;
|
||||
else bedStats.available++;
|
||||
});
|
||||
selectedBedCount.value = rows.filter((r) => r.isBed).length;
|
||||
selectedBedStats.value = bedStats;
|
||||
};
|
||||
|
||||
/** 打开选择性分配弹窗 */
|
||||
const selectiveDialog = reactive({ visible: false, title: '分配床位给学生' });
|
||||
const openSelectiveAllocation = () => {
|
||||
if (!selectedBuilding.value) {
|
||||
ElMessage.warning('请先选择宿舍楼');
|
||||
return;
|
||||
}
|
||||
selectiveDialog.visible = true;
|
||||
};
|
||||
|
||||
/** 分配成功回调(刷新) */
|
||||
const handleAllocationSuccess = () => {
|
||||
loadRoomData();
|
||||
};
|
||||
|
||||
/** 一键随机分配 */
|
||||
const handleRandomAllocation = async () => {
|
||||
if (!selectedBuilding.value) return ElMessage.warning('请先选择宿舍楼');
|
||||
try {
|
||||
await ElMessageBox.confirm('确认对本班级在该楼栋内进行随机分配吗?', '提示', { type: 'warning' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
actionLoading.value = true;
|
||||
await classTeacherRandomAllocation(selectedBuilding.value.id as any, activeClassId.value as any);
|
||||
ElMessage.success('随机分配成功');
|
||||
loadRoomData();
|
||||
} catch (e) {
|
||||
ElMessage.error('随机分配失败');
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 按学号分配 */
|
||||
const handleStudentNoAllocation = async () => {
|
||||
if (!selectedBuilding.value) return ElMessage.warning('请先选择宿舍楼');
|
||||
try {
|
||||
await ElMessageBox.confirm('确认按学号顺序为学生分配床位吗?', '提示', { type: 'warning' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
actionLoading.value = true;
|
||||
await classTeacherStudentNoAllocation(selectedBuilding.value.id as any, activeClassId.value as any);
|
||||
ElMessage.success('按学号分配成功');
|
||||
loadRoomData();
|
||||
} catch (e) {
|
||||
ElMessage.error('按学号分配失败');
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 一键收回 */
|
||||
const handleRecall = async () => {
|
||||
if (!selectedBuilding.value) return ElMessage.warning('请先选择宿舍楼');
|
||||
try {
|
||||
await ElMessageBox.confirm('确认收回本班级在该楼栋内的所有床位分配吗?', '提示', { type: 'warning' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
actionLoading.value = true;
|
||||
await classTeacherRecallBeds(selectedBuilding.value.id as any, activeClassId.value as any);
|
||||
ElMessage.success('收回成功');
|
||||
loadRoomData();
|
||||
} catch (e) {
|
||||
ElMessage.error('收回失败');
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadMyClassList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.flex-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.tree-sidebar {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tree-card {
|
||||
flex: 1;
|
||||
max-height: calc(50vh - 24px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tree-card :deep(.el-card__body) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex-grow: 1;
|
||||
overflow: visible;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.el-table :deep(.el-table__cell) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.el-table :deep(.el-table__fixed-right) {
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-card :deep(.el-table) {
|
||||
width: 100% !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.el-card :deep(.el-table__body-wrapper) {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.el-card :deep(.el-table__header-wrapper) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.el-card :deep(.el-table__body),
|
||||
.el-card :deep(.el-table__header) {
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-card :deep(.el-table__inner-wrapper) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.card-header-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.custom-tree-node {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.disabled-node {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.male-building {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
.female-building {
|
||||
color: #ff69b4;
|
||||
font-weight: bold;
|
||||
}
|
||||
.female-tag {
|
||||
background-color: rgba(255, 105, 180, 0.1) !important;
|
||||
color: #ff69b4 !important;
|
||||
border-color: rgba(255, 105, 180, 0.1) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -95,9 +95,9 @@
|
||||
<template #header>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" icon="UserFilled" size="small" @click="handleSelectiveAllocation">选择分配</el-button>
|
||||
<el-button type="success" icon="Shuffle" size="small" @click="handleRandomAllocation">随机分配</el-button>
|
||||
<el-button type="info" icon="Sort" size="small" @click="handleStudentNoAllocation">按学号分配</el-button>
|
||||
<el-button type="primary" icon="UserFilled" size="small" @click="handleSelectiveAllocation">分配床位</el-button>
|
||||
<!-- <el-button type="success" icon="UserFilled" size="small" @click="handleRandomAllocation">随机分配</el-button>
|
||||
<el-button type="info" icon="Sort" size="small" @click="handleStudentNoAllocation">按学号分配</el-button> -->
|
||||
<el-button type="warning" icon="RefreshLeft" size="small" @click="handleRecallBeds">收回床位</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right">
|
||||
@@ -136,6 +136,16 @@
|
||||
- 床位分配管理</span
|
||||
>
|
||||
<div class="text-blue-500 flex flex-wrap items-center gap-2">
|
||||
<span v-if="unallocatedStudentStats.total > 0" class="text-orange-500 text-sm">
|
||||
未分配学生:
|
||||
<el-tag size="small" type="warning">{{ unallocatedStudentStats.total }}</el-tag
|
||||
>人
|
||||
<span class="ml-2 text-xs">
|
||||
[男生 <el-tag size="small" type="primary">{{ unallocatedStudentStats.male }}</el-tag
|
||||
>人, 女生 <el-tag size="small" class="female-tag">{{ unallocatedStudentStats.female }}</el-tag
|
||||
>人]
|
||||
</span>
|
||||
</span>
|
||||
<span class="whitespace-nowrap"> 已选择: {{ selectedRoomCount }}个房间 / {{ selectedBedCount }}个床位 </span>
|
||||
<span v-if="selectedBedCount > 0" class="whitespace-nowrap text-sm">
|
||||
[可分配: <el-tag size="small" type="success">{{ selectedBedStats.available }}</el-tag
|
||||
@@ -143,10 +153,6 @@
|
||||
>个, 维修中: <el-tag size="small" type="danger">{{ selectedBedStats.maintenance }}</el-tag
|
||||
>个]
|
||||
</span>
|
||||
<span v-if="unallocatedStudentCount > 0" class="text-orange-500 text-sm">
|
||||
未分配学生: <el-tag size="small" type="warning">{{ unallocatedStudentCount }}</el-tag
|
||||
>人
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -170,10 +176,16 @@
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="160">
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="180">
|
||||
<template #default="scope">
|
||||
<span v-if="!scope.row.isBed">{{ scope.row.roomName }} (房间)</span>
|
||||
<span v-else>{{ scope.row.bedName }} (床位)</span>
|
||||
<span v-if="!scope.row.isBed" class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><House /></el-icon>
|
||||
{{ scope.row.roomName }}
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
{{ scope.row.bedName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="floorNum" label="楼层" width="80" align="center">
|
||||
@@ -277,6 +289,7 @@
|
||||
:title="selectiveAllocationDialog.title"
|
||||
:building-info="selectiveAllocationDialog.buildingInfo"
|
||||
:class-id="activeClassId"
|
||||
:parent-class-info="currentClassInfo"
|
||||
@success="handleAllocationSuccess"
|
||||
/>
|
||||
</div>
|
||||
@@ -284,16 +297,18 @@
|
||||
|
||||
<script setup name="ClassTeacherBedAllocation" lang="ts">
|
||||
import { ref, computed, reactive, onMounted, nextTick, watch } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { InfoFilled, House, Avatar } from '@element-plus/icons-vue';
|
||||
import type { DormBuildingTreeVO } from '@/api/xg/xgss/dormBedStudent/dormBedStudent/types';
|
||||
import {
|
||||
getClassTeacherDormTree,
|
||||
getClassTeacherBedDetails,
|
||||
getClassTeacherUnallocatedStudents,
|
||||
classTeacherRandomAllocation,
|
||||
classTeacherStudentNoAllocation,
|
||||
classTeacherRecallBeds
|
||||
} from '@/api/xg/xgss/dormBedStudent/dormBedStudent';
|
||||
// 导入API方法
|
||||
import { listDormRoomByClassId } from '@/api/xg/xgss/dormRoom/index';
|
||||
import { getBedDetailsByRoomIdAndClassId } from '@/api/xg/xgss/dormBed/index';
|
||||
import { getMyClassListAsTeacher } from '@/api/xg/xsjcxx/classMsg/classMsg';
|
||||
// 定义一个最小班级信息类型以避免跨文件类型不兼容
|
||||
interface SimpleClassVO {
|
||||
@@ -329,6 +344,11 @@ const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve,
|
||||
const classList = ref<SimpleClassVO[]>([]);
|
||||
const activeClassId = ref<string>('');
|
||||
|
||||
// 当前选中的班级信息
|
||||
const currentClassInfo = computed(() => {
|
||||
return classList.value.find((cls) => String(cls.id) === activeClassId.value) || null;
|
||||
});
|
||||
|
||||
// 选择状态
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const selectedRoomCount = ref(0);
|
||||
@@ -342,6 +362,12 @@ const selectedBedStats = ref({
|
||||
|
||||
// 未分配学生数量
|
||||
const unallocatedStudentCount = ref(0);
|
||||
// 按性别统计的未分配学生数量
|
||||
const unallocatedStudentStats = ref({
|
||||
male: 0, // 男生数量
|
||||
female: 0, // 女生数量
|
||||
total: 0 // 总数量
|
||||
});
|
||||
|
||||
const animate = reactive({
|
||||
searchAnimate: {
|
||||
@@ -432,6 +458,7 @@ const initForActiveClass = async () => {
|
||||
selectedBedCount.value = 0;
|
||||
selectedBedStats.value = { available: 0, allocated: 0, maintenance: 0 };
|
||||
unallocatedStudentCount.value = 0;
|
||||
unallocatedStudentStats.value = { male: 0, female: 0, total: 0 };
|
||||
// 重新加载宿舍树
|
||||
await getDormTreeData();
|
||||
};
|
||||
@@ -485,18 +512,72 @@ const resetQuery = () => {
|
||||
|
||||
/** 加载房间数据 */
|
||||
const loadRoomData = async () => {
|
||||
if (!queryParams.buildingId) return;
|
||||
if (!queryParams.buildingId || !activeClassId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
const controller = currentController;
|
||||
|
||||
try {
|
||||
// 获取班主任管理班级的房间床位详情
|
||||
const res = await getClassTeacherBedDetails(queryParams.buildingId, activeClassId.value);
|
||||
// 构建查询参数
|
||||
const query = {
|
||||
buildingId: queryParams.buildingId,
|
||||
roomName: queryParams.roomName,
|
||||
roomStatus: queryParams.roomStatus,
|
||||
pageNum: queryParams.pageNum,
|
||||
pageSize: queryParams.pageSize
|
||||
};
|
||||
|
||||
// 获取指定班级的宿舍房间信息列表
|
||||
const res = await listDormRoomByClassId(activeClassId.value, query);
|
||||
const rooms = res.rows || [];
|
||||
|
||||
if (controller?.signal?.aborted) return;
|
||||
|
||||
roomList.value = res.data || [];
|
||||
// 并行获取所有房间的床位详细信息
|
||||
const roomPromises = rooms.map(async (room) => {
|
||||
// 创建房间记录
|
||||
const roomRecord = {
|
||||
...room,
|
||||
isBed: false,
|
||||
children: []
|
||||
};
|
||||
|
||||
try {
|
||||
// 调用新的接口获取指定房间和班级的床位详细信息
|
||||
const bedRes = await getBedDetailsByRoomIdAndClassId(room.id, activeClassId.value);
|
||||
|
||||
// 如果请求已被取消,不继续处理
|
||||
if (controller?.signal?.aborted) return roomRecord;
|
||||
|
||||
const beds = bedRes.data || [];
|
||||
|
||||
// 将床位信息转换为符合表格展示的格式
|
||||
if (beds.length > 0) {
|
||||
roomRecord.children = beds.map((bed) => ({
|
||||
...bed,
|
||||
isBed: true,
|
||||
roomName: room.roomName // 保留房间名称,方便展示
|
||||
}));
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 忽略请求被取消的错误
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error(`获取房间 ${room.id} 的床位详细信息失败`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return roomRecord;
|
||||
});
|
||||
|
||||
// 等待所有请求完成
|
||||
const tempList = await Promise.all(roomPromises);
|
||||
|
||||
// 如果请求已被取消,不更新UI
|
||||
if (controller?.signal?.aborted) return;
|
||||
|
||||
roomList.value = tempList;
|
||||
|
||||
console.log('房间数据:', tempList);
|
||||
|
||||
// 构建床位到房间的映射缓存
|
||||
buildBedToRoomMap();
|
||||
@@ -520,6 +601,12 @@ const loadRoomData = async () => {
|
||||
isUpdatingSelection.value = false;
|
||||
}
|
||||
|
||||
// 自动展开所有房间行,这样用户可以立即看到床位信息
|
||||
await nextTick();
|
||||
await nextTick(); // 双重 nextTick 确保 DOM 完全更新
|
||||
expandedRowKeys.value = tempList.map((room) => String(room.id));
|
||||
console.log('设置展开的房间ID:', expandedRowKeys.value);
|
||||
|
||||
// 获取未分配学生数量
|
||||
await getUnallocatedStudentCount();
|
||||
} catch (error: any) {
|
||||
@@ -537,7 +624,22 @@ const loadRoomData = async () => {
|
||||
const getUnallocatedStudentCount = async () => {
|
||||
try {
|
||||
const res = await getClassTeacherUnallocatedStudents(activeClassId.value);
|
||||
unallocatedStudentCount.value = res.data?.length || 0;
|
||||
const students = res.data || [];
|
||||
|
||||
// 按性别统计
|
||||
const maleCount = students.filter((student) => student.gender === '0').length;
|
||||
const femaleCount = students.filter((student) => student.gender === '1').length;
|
||||
const totalCount = students.length;
|
||||
|
||||
// 更新统计数据
|
||||
unallocatedStudentStats.value = {
|
||||
male: maleCount,
|
||||
female: femaleCount,
|
||||
total: totalCount
|
||||
};
|
||||
|
||||
// 保持向后兼容
|
||||
unallocatedStudentCount.value = totalCount;
|
||||
} catch (error) {
|
||||
console.error('获取未分配学生数量失败', error);
|
||||
}
|
||||
@@ -638,8 +740,15 @@ const handleRecallBeds = async () => {
|
||||
const handleAllocationSuccess = () => {
|
||||
// 清除缓存,确保数据刷新
|
||||
selectableRowsCache.value = [];
|
||||
// 保存当前展开状态
|
||||
const currentExpandedKeys = [...expandedRowKeys.value];
|
||||
// 刷新房间和床位数据
|
||||
loadRoomData();
|
||||
loadRoomData().then(() => {
|
||||
// 数据刷新后,恢复展开状态
|
||||
nextTick(() => {
|
||||
expandedRowKeys.value = currentExpandedKeys;
|
||||
});
|
||||
});
|
||||
// 清空选择状态
|
||||
isUpdatingSelection.value = true;
|
||||
try {
|
||||
@@ -647,6 +756,7 @@ const handleAllocationSuccess = () => {
|
||||
selectedRoomCount.value = 0;
|
||||
selectedBedCount.value = 0;
|
||||
selectedBedStats.value = { available: 0, allocated: 0, maintenance: 0 };
|
||||
unallocatedStudentStats.value = { male: 0, female: 0, total: 0 };
|
||||
} finally {
|
||||
isUpdatingSelection.value = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="title" width="1200px" :before-close="handleClose" destroy-on-close class="selective-allocation-dialog">
|
||||
<div class="allocation-container">
|
||||
<!-- 左侧:床位选择区域 -->
|
||||
<div class="selection-area">
|
||||
<div class="area-header">
|
||||
<h3 class="area-title">
|
||||
<el-icon><House /></el-icon>
|
||||
可用床位选择
|
||||
</h3>
|
||||
<div class="stats">
|
||||
已选择 <el-tag type="primary" size="small">{{ selectedBeds.length }}</el-tag> 个床位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input v-model="bedSearchKey" placeholder="搜索房间号、床位号" prefix-icon="Search" clearable @input="handleBedSearch" />
|
||||
</div>
|
||||
|
||||
<div class="bed-list" v-loading="bedLoading">
|
||||
<el-table
|
||||
ref="bedTableRef"
|
||||
:data="filteredBedList"
|
||||
row-key="id"
|
||||
height="400"
|
||||
@selection-change="handleBedSelectionChange"
|
||||
@select-all="handleBedSelectAll"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkBedSelectable" />
|
||||
<el-table-column prop="roomName" label="房间" width="100" />
|
||||
<el-table-column prop="bedName" label="床位" width="100" />
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.bedType === '1'" type="success" size="small">下铺</el-tag>
|
||||
<el-tag v-else-if="scope.row.bedType === '2'" type="warning" size="small">上铺</el-tag>
|
||||
<el-tag v-else-if="scope.row.bedType === '3'" type="danger" size="small">中铺</el-tag>
|
||||
<el-tag v-else type="info" size="small">独立床</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default>
|
||||
<el-tag type="success" size="small">空闲</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:操作区域 -->
|
||||
<div class="operation-area">
|
||||
<div class="match-info">
|
||||
<el-icon class="match-icon"><Connection /></el-icon>
|
||||
<div class="match-stats">
|
||||
<div class="stat-item">
|
||||
<span class="label">床位:</span>
|
||||
<span class="value">{{ selectedBeds.length }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">学生:</span>
|
||||
<span class="value">{{ selectedStudents.length }}</span>
|
||||
</div>
|
||||
<div class="match-status" :class="{ 'matched': isMatched, 'unmatched': !isMatched }">
|
||||
{{ isMatched ? '✓ 数量匹配' : '✗ 数量不匹配' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="allocation-buttons">
|
||||
<el-button type="success" icon="Refresh" :disabled="!isMatched" @click="handleRandomAllocation"> 随机分配 </el-button>
|
||||
<el-button type="primary" icon="Sort" :disabled="!isMatched" @click="handleStudentNoAllocation"> 按学号分配 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:学生选择区域 -->
|
||||
<div class="selection-area">
|
||||
<div class="area-header">
|
||||
<h3 class="area-title">
|
||||
<el-icon><User /></el-icon>
|
||||
未分配学生选择
|
||||
</h3>
|
||||
<div class="stats">
|
||||
已选择 <el-tag type="warning" size="small">{{ selectedStudents.length }}</el-tag> 名学生
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input v-model="studentSearchKey" placeholder="搜索学号、姓名" prefix-icon="Search" clearable @input="handleStudentSearch" />
|
||||
</div>
|
||||
|
||||
<div class="student-list" v-loading="studentLoading">
|
||||
<el-table
|
||||
ref="studentTableRef"
|
||||
:data="filteredStudentList"
|
||||
row-key="id"
|
||||
height="400"
|
||||
@selection-change="handleStudentSelectionChange"
|
||||
@select-all="handleStudentSelectAll"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="studentNo" label="学号" width="120" />
|
||||
<el-table-column prop="studentName" label="姓名" width="100" />
|
||||
<el-table-column label="性别" width="60" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.studentGender === '0'" type="primary" size="small">男</el-tag>
|
||||
<el-tag v-else-if="scope.row.studentGender === '1'" class="female-tag" size="small">女</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="className" label="班级" min-width="120" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!isMatched">确认分配</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { House, User, Connection } from '@element-plus/icons-vue';
|
||||
import type { DormBuildingTreeVO } from '@/api/xg/xgss/dormBedStudent/dormBedStudent/types';
|
||||
import {
|
||||
getClassTeacherBedDetails,
|
||||
getClassTeacherUnallocatedStudents,
|
||||
classTeacherSelectiveAllocation
|
||||
} from '@/api/xg/xgss/dormBedStudent/dormBedStudent';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
|
||||
// 定义组件选项
|
||||
defineOptions({
|
||||
name: 'SelectiveAllocationDialog'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
title?: string;
|
||||
buildingInfo?: DormBuildingTreeVO;
|
||||
classId?: string | number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'success'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '选择性分配床位'
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 对话框显示状态
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
});
|
||||
|
||||
// 加载状态
|
||||
const bedLoading = ref(false);
|
||||
const studentLoading = ref(false);
|
||||
|
||||
// 搜索关键字
|
||||
const bedSearchKey = ref('');
|
||||
const studentSearchKey = ref('');
|
||||
|
||||
// 表格引用
|
||||
const bedTableRef = ref();
|
||||
const studentTableRef = ref();
|
||||
|
||||
// 数据列表
|
||||
const bedList = ref<any[]>([]);
|
||||
const studentList = ref<any[]>([]);
|
||||
const selectedBeds = ref<any[]>([]);
|
||||
const selectedStudents = ref<any[]>([]);
|
||||
|
||||
// 计算属性:过滤后的列表
|
||||
const filteredBedList = computed(() => {
|
||||
if (!bedSearchKey.value) return bedList.value;
|
||||
const key = bedSearchKey.value.toLowerCase();
|
||||
return bedList.value.filter((bed) => (bed.roomName || '').toLowerCase().includes(key) || (bed.bedName || '').toLowerCase().includes(key));
|
||||
});
|
||||
|
||||
const filteredStudentList = computed(() => {
|
||||
if (!studentSearchKey.value) return studentList.value;
|
||||
const key = studentSearchKey.value.toLowerCase();
|
||||
return studentList.value.filter(
|
||||
(student) => (student.studentNo || '').toLowerCase().includes(key) || (student.studentName || '').toLowerCase().includes(key)
|
||||
);
|
||||
});
|
||||
|
||||
// 计算属性:是否匹配
|
||||
const isMatched = computed(() => {
|
||||
return selectedBeds.value.length > 0 && selectedStudents.value.length > 0 && selectedBeds.value.length === selectedStudents.value.length;
|
||||
});
|
||||
|
||||
// 监听建筑信息变化,加载数据
|
||||
watch(
|
||||
() => props.buildingInfo,
|
||||
async (newValue) => {
|
||||
if (newValue && props.modelValue) {
|
||||
await loadData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听对话框打开,加载数据
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (newValue) => {
|
||||
if (newValue && props.buildingInfo) {
|
||||
await loadData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/** 加载数据 */
|
||||
const loadData = async () => {
|
||||
await Promise.all([loadBedData(), loadStudentData()]);
|
||||
};
|
||||
|
||||
/** 加载床位数据 */
|
||||
const loadBedData = async () => {
|
||||
if (!props.buildingInfo) return;
|
||||
|
||||
bedLoading.value = true;
|
||||
try {
|
||||
const res = await getClassTeacherBedDetails(props.buildingInfo.id as any, props.classId as any);
|
||||
const allBeds: any[] = [];
|
||||
(res.data || []).forEach((room: any) => {
|
||||
if (room.children && room.children.length > 0) {
|
||||
room.children.forEach((bed: any) => {
|
||||
// 只要空闲床位(没有学生分配)
|
||||
if (!bed.studentId && bed.bedStatus !== '2') {
|
||||
allBeds.push({
|
||||
...bed,
|
||||
roomName: room.roomName
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
bedList.value = allBeds;
|
||||
} catch (error) {
|
||||
console.error('获取床位数据失败', error);
|
||||
ElMessage.error('获取床位数据失败');
|
||||
} finally {
|
||||
bedLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 加载学生数据 */
|
||||
const loadStudentData = async () => {
|
||||
studentLoading.value = true;
|
||||
try {
|
||||
const res = await getClassTeacherUnallocatedStudents(props.classId as any);
|
||||
studentList.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取学生数据失败', error);
|
||||
ElMessage.error('获取学生数据失败');
|
||||
} finally {
|
||||
studentLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 床位搜索 */
|
||||
const handleBedSearch = () => {};
|
||||
/** 学生搜索 */
|
||||
const handleStudentSearch = () => {};
|
||||
|
||||
/** 检查床位是否可选 */
|
||||
const checkBedSelectable = (row: any) => {
|
||||
return !row.studentId && row.bedStatus !== '2';
|
||||
};
|
||||
|
||||
/** 床位选择变化 */
|
||||
const handleBedSelectionChange = (selection: any[]) => {
|
||||
selectedBeds.value = selection;
|
||||
};
|
||||
/** 床位全选 */
|
||||
const handleBedSelectAll = (selection: any[]) => {
|
||||
selectedBeds.value = selection;
|
||||
};
|
||||
|
||||
/** 学生选择变化 */
|
||||
const handleStudentSelectionChange = (selection: any[]) => {
|
||||
selectedStudents.value = selection;
|
||||
};
|
||||
/** 学生全选 */
|
||||
const handleStudentSelectAll = (selection: any[]) => {
|
||||
selectedStudents.value = selection;
|
||||
};
|
||||
|
||||
/** 随机分配 */
|
||||
const handleRandomAllocation = async () => {
|
||||
if (!isMatched.value) {
|
||||
ElMessage.warning('请确保床位和学生数量匹配');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const allocationParams = {
|
||||
bedIds: selectedBeds.value.map((bed) => bed.id),
|
||||
studentIds: selectedStudents.value.map((student) => student.id),
|
||||
allocationType: 'random' as const
|
||||
};
|
||||
|
||||
await classTeacherSelectiveAllocation(allocationParams, props.classId as any);
|
||||
ElMessage.success('随机分配成功');
|
||||
handleSuccess();
|
||||
} catch (error) {
|
||||
ElMessage.error('随机分配失败');
|
||||
}
|
||||
};
|
||||
|
||||
/** 按学号分配 */
|
||||
const handleStudentNoAllocation = async () => {
|
||||
if (!isMatched.value) {
|
||||
ElMessage.warning('请确保床位和学生数量匹配');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const allocationParams = {
|
||||
bedIds: selectedBeds.value.map((bed) => bed.id),
|
||||
studentIds: selectedStudents.value.map((student) => student.id),
|
||||
allocationType: 'studentNo' as const
|
||||
};
|
||||
|
||||
await classTeacherSelectiveAllocation(allocationParams, props.classId as any);
|
||||
ElMessage.success('按学号分配成功');
|
||||
handleSuccess();
|
||||
} catch (error) {
|
||||
ElMessage.error('按学号分配失败');
|
||||
}
|
||||
};
|
||||
|
||||
/** 确认分配(默认随机) */
|
||||
const handleConfirm = () => {
|
||||
handleRandomAllocation();
|
||||
};
|
||||
|
||||
/** 分配成功处理 */
|
||||
const handleSuccess = () => {
|
||||
emit('success');
|
||||
handleClose();
|
||||
};
|
||||
|
||||
/** 关闭对话框 */
|
||||
const handleClose = () => {
|
||||
selectedBeds.value = [];
|
||||
selectedStudents.value = [];
|
||||
bedSearchKey.value = '';
|
||||
studentSearchKey.value = '';
|
||||
bedList.value = [];
|
||||
studentList.value = [];
|
||||
|
||||
nextTick(() => {
|
||||
bedTableRef.value?.clearSelection();
|
||||
studentTableRef.value?.clearSelection();
|
||||
});
|
||||
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selective-allocation-dialog :deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.allocation-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.selection-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.area-header {
|
||||
background: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.area-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.bed-list,
|
||||
.student-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.operation-area {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.match-info {
|
||||
text-align: center;
|
||||
}
|
||||
.match-icon {
|
||||
font-size: 32px;
|
||||
color: #909399;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.match-stats {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.label {
|
||||
color: #606266;
|
||||
}
|
||||
.value {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.match-status {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.match-status.matched {
|
||||
background: #f0f9ff;
|
||||
color: #67c23a;
|
||||
border: 1px solid #b3e19d;
|
||||
}
|
||||
.match-status.unmatched {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.allocation-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.allocation-buttons .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.female-tag {
|
||||
background-color: rgba(255, 105, 180, 0.1) !important;
|
||||
color: #ff69b4 !important;
|
||||
border-color: rgba(255, 105, 180, 0.1) !important;
|
||||
}
|
||||
|
||||
.selection-area :deep(.el-table) {
|
||||
border: none;
|
||||
}
|
||||
.selection-area :deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.selection-area :deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
.selection-area :deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.selection-area :deep(.el-table__row:hover > td) {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -55,7 +55,7 @@
|
||||
|
||||
<!-- 学生树 -->
|
||||
<el-card shadow="hover" class="tree-card mt-4">
|
||||
<div class="card-header-title">学生信息</div>
|
||||
<div class="card-header-title">班级床位需求信息(已分配/总需求)</div>
|
||||
<el-input
|
||||
v-model="studentSearchKey"
|
||||
prefix-icon="Search"
|
||||
@@ -205,10 +205,16 @@
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="160">
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="180">
|
||||
<template #default="scope">
|
||||
<span v-if="!scope.row.isBed">{{ scope.row.roomName }} (房间)</span>
|
||||
<span v-else>{{ scope.row.bedName }} (床位)</span>
|
||||
<span v-if="!scope.row.isBed" class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><House /></el-icon>
|
||||
{{ scope.row.roomName }}
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
{{ scope.row.bedName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="floorNum" label="楼层" width="80" align="center">
|
||||
@@ -352,7 +358,7 @@
|
||||
|
||||
<script setup name="DormRoomTreeManagement" lang="ts">
|
||||
import { ref, computed, reactive, onMounted, nextTick, watch, getCurrentInstance } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { InfoFilled, House, Avatar } from '@element-plus/icons-vue';
|
||||
import type { DormBuildingTreeVO } from '@/api/xg/xgss/dormBedStudent/dormBedStudent/types';
|
||||
import type { DormTreeNode, BedInfo } from '@/api/xg/xgss/xsssfjgl/types';
|
||||
import type { DormRoomVO } from '@/api/xg/xgss/dormRoom/types';
|
||||
|
||||
@@ -205,10 +205,16 @@
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="160">
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="180">
|
||||
<template #default="scope">
|
||||
<span v-if="!scope.row.isBed">{{ scope.row.roomName }} (房间)</span>
|
||||
<span v-else>{{ scope.row.bedName }} (床位)</span>
|
||||
<span v-if="!scope.row.isBed" class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><House /></el-icon>
|
||||
{{ scope.row.roomName }}
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
{{ scope.row.bedName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="floorNum" label="楼层" width="80" align="center">
|
||||
@@ -350,7 +356,7 @@
|
||||
|
||||
<script setup name="DormRoomTreeManagement" lang="ts">
|
||||
import { ref, computed, reactive, onMounted, nextTick, watch, getCurrentInstance } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { InfoFilled, House, Avatar } from '@element-plus/icons-vue';
|
||||
import type { DormTreeNode, BedInfo } from '@/api/xg/xgss/xsssfjgl/types';
|
||||
import type { DormRoomVO } from '@/api/xg/xgss/dormRoom/types';
|
||||
import type { DormBedDetailVO } from '@/api/xg/xgss/dormBed/types';
|
||||
|
||||
Reference in new issue
Block a user