提交代码

This commit is contained in:
zlwu committed 2025-08-14 18:13:34 +08:00
1 parent 1db2f91af1
commit 574660ce9f
20 files changed
+2790 -144

No files matched your search

@@ -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);
}
}
@@ -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));
}
@@ -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);
}
/**
* 导出宿舍房间信息列表
*/
@@ -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;
}
@@ -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);
}
@@ -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);
}
@@ -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);
/**
* 班主任一键收回床位
@@ -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);
/**
* 查询符合条件的宿舍房间信息列表
*
@@ -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-的就是二级学院
@@ -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;
}
/**
* 班主任一键收回床位
*
@@ -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` 判定床位是否已入住,并展示学生信息
> 本次为前端界面与交互完善,无数据库表结构与后端接口变更。文档同步记录以便后续联调与测试追溯。
+12
View File
@@ -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 批量分配参数
+26
View File
@@ -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
+685
View File
@@ -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>
+128 -18
View File
@@ -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
+11 -5
View File
@@ -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';
+10 -4
View File
@@ -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';