zlwu 1 год назад
Родитель
Сommit
574660ce9f
20 измененных файлов с 2777 добавлено и 131 удалено
  1. 15 0
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedController.java
  2. 2 1
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedStudentController.java
  3. 12 0
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRoomController.java
  4. 35 0
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormSelectiveAllocationBo.java
  5. 59 0
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedMapper.java
  6. 9 0
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedService.java
  7. 2 1
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedStudentService.java
  8. 10 0
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRoomService.java
  9. 12 0
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java
  10. 461 6
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java
  11. 55 0
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java
  12. 29 1
      docs/业务模块/学工管理-宿舍管理/README.md
  13. 12 0
      web/src/api/xg/xgss/dormBed/index.ts
  14. 26 0
      web/src/api/xg/xgss/dormRoom/index.ts
  15. 685 0
      web/src/views/xg/xgss/bzrfp/bzrfp-bak1.vue
  16. 128 18
      web/src/views/xg/xgss/bzrfp/bzrfp.vue
  17. 519 0
      web/src/views/xg/xgss/bzrfp/components/SelectiveAllocationDialog-bak1.vue
  18. 685 95
      web/src/views/xg/xgss/bzrfp/components/SelectiveAllocationDialog.vue
  19. 11 5
      web/src/views/xg/xgss/ejxyfp/ejxyfp.vue
  20. 10 4
      web/src/views/xg/xgss/xsssfpejxy/index.vue

+ 15 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedController.java

@@ -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
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedStudentController.java

@@ -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 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRoomController.java

@@ -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
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormSelectiveAllocationBo.java

@@ -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 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgDormBedMapper.java

@@ -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 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedService.java

@@ -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
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedStudentService.java

@@ -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 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRoomService.java

@@ -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 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedServiceImpl.java

@@ -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
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java

@@ -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 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java

@@ -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();
+        }
+    }
 }

+ 29 - 1
docs/业务模块/学工管理-宿舍管理/README.md

@@ -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 - 0
web/src/api/xg/xgss/dormBed/index.ts

@@ -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 - 0
web/src/api/xg/xgss/dormRoom/index.ts

@@ -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 - 0
web/src/views/xg/xgss/bzrfp/bzrfp-bak1.vue

@@ -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
web/src/views/xg/xgss/bzrfp/bzrfp.vue

@@ -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;
+
+    // 并行获取所有房间的床位详细信息
+    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 = res.data || [];
+    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;
   }

+ 519 - 0
web/src/views/xg/xgss/bzrfp/components/SelectiveAllocationDialog-bak1.vue

@@ -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>

+ 685 - 95
web/src/views/xg/xgss/bzrfp/components/SelectiveAllocationDialog.vue

@@ -7,6 +7,9 @@
           <h3 class="area-title">
             <el-icon><House /></el-icon>
             可用床位选择
+            <el-tag v-if="props.buildingInfo?.buildingType === '1'" type="primary" size="small">男生楼</el-tag>
+            <el-tag v-else-if="props.buildingInfo?.buildingType === '2'" class="female-tag" size="small">女生楼</el-tag>
+            <el-tag v-else-if="props.buildingInfo?.buildingType === '3'" type="info" size="small">混合楼</el-tag>
           </h3>
           <div class="stats">
             已选择 <el-tag type="primary" size="small">{{ selectedBeds.length }}</el-tag> 个床位
@@ -22,24 +25,74 @@
             ref="bedTableRef"
             :data="filteredBedList"
             row-key="id"
-            height="400"
+            height="330"
+            max-height="330"
+            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+            :expand-row-keys="expandRowKeys"
+            @expand-change="handleExpandChange"
+            :row-class-name="getRowClassName"
             @selection-change="handleBedSelectionChange"
+            @select="handleRowSelection"
             @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 prop="roomName" label="房间/床位" min-width="140">
+              <template #default="scope">
+                <div v-if="!scope.row.isBed" class="room-node">
+                  <el-icon><House /></el-icon>
+                  <span>{{ scope.row.roomName }}</span>
+                  <!-- <el-tag v-if="scope.row.roomType === '1'" type="primary" size="small">男生房间</el-tag>
+                  <el-tag v-else-if="scope.row.roomType === '2'" class="female-tag" size="small">女生房间</el-tag> -->
+                </div>
+                <div v-else class="bed-node">
+                  <el-icon><Avatar /></el-icon>
+                  <span class="bed-name">{{ scope.row.bedName }}</span>
+                </div>
+              </template>
+            </el-table-column>
             <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 v-if="scope.row.isBed">
+                  <el-tag v-if="scope.row.bedType === '1'" type="success" size="small">下铺</el-tag>
+                  <el-tag v-if="scope.row.bedType === '2'" type="warning" size="small">上铺</el-tag>
+                  <el-tag v-if="scope.row.bedType === '3'" type="danger" size="small">中铺</el-tag>
+                  <el-tag v-if="!scope.row.bedType || scope.row.bedType === '4'" type="info" size="small">独立床</el-tag>
+                </template>
+                <span v-else class="text-gray-400">
+                  <span>{{ scope.row.roomType === '1' ? '标准间' : scope.row.roomType === '2' ? '单人间' : '套间' }}</span>
+                </span>
               </template>
             </el-table-column>
             <el-table-column label="状态" width="80" align="center">
-              <template #default>
-                <el-tag type="success" size="small">空闲</el-tag>
+              <template #default="scope">
+                <template v-if="scope.row.isBed">
+                  <el-tag type="success" size="small">空闲</el-tag>
+                </template>
+                <span v-else class="text-gray-400">—</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="学生" width="80" align="center">
+              <template #default="scope">
+                <span v-if="scope.row.isBed && scope.row.studentName">{{ scope.row.studentName }}</span>
+                <span v-else-if="scope.row.isBed" class="text-gray-400">—</span>
+                <span v-else class="text-gray-400">—</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="学号" width="100" align="center">
+              <template #default="scope">
+                <span v-if="scope.row.isBed && scope.row.studentNo">{{ scope.row.studentNo }}</span>
+                <span v-else-if="scope.row.isBed" class="text-gray-400">—</span>
+                <span v-else class="text-gray-400">—</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="性别" width="60" align="center">
+              <template #default="scope">
+                <template 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>
+                </template>
+                <span v-else-if="scope.row.isBed" class="text-gray-400">—</span>
+                <span v-else class="text-gray-400">—</span>
               </template>
             </el-table-column>
           </el-table>
@@ -48,6 +101,31 @@
 
       <!-- 中间:操作区域 -->
       <div class="operation-area">
+        <!-- 班级和楼栋信息显示 -->
+        <div class="allocation-info">
+          <div class="info-header">
+            <el-icon class="info-icon"><Avatar /></el-icon>
+            <span class="info-title">分配信息</span>
+          </div>
+          <div class="info-content">
+            <div class="info-item">
+              <span class="info-label">班级:</span>
+              <span class="info-value">{{ classInfo?.className || '未知班级' }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">楼栋:</span>
+              <span class="info-value">{{ props.buildingInfo?.label || '未选择楼栋' }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">类型:</span>
+              <el-tag v-if="props.buildingInfo?.buildingType === '1'" type="primary" size="small">男生楼</el-tag>
+              <el-tag v-else-if="props.buildingInfo?.buildingType === '2'" class="female-tag" size="small">女生楼</el-tag>
+              <el-tag v-else-if="props.buildingInfo?.buildingType === '3'" type="info" size="small">混合楼</el-tag>
+              <span v-else class="info-value">未知类型</span>
+            </div>
+          </div>
+        </div>
+
         <div class="match-info">
           <el-icon class="match-icon"><Connection /></el-icon>
           <div class="match-stats">
@@ -60,14 +138,19 @@
               <span class="value">{{ selectedStudents.length }}</span>
             </div>
             <div class="match-status" :class="{ 'matched': isMatched, 'unmatched': !isMatched }">
-              {{ 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 class="allocation-options">
+            <div class="option-title">分配方式:</div>
+            <el-radio-group v-model="allocationType" :disabled="!isMatched">
+              <el-radio value="random">随机分配</el-radio>
+              <el-radio value="studentNo">按学号分配</el-radio>
+            </el-radio-group>
+          </div>
         </div>
       </div>
 
@@ -77,6 +160,9 @@
           <h3 class="area-title">
             <el-icon><User /></el-icon>
             未分配学生选择
+            <el-tag v-if="props.buildingInfo?.buildingType === '1'" type="primary" size="small">仅男学生</el-tag>
+            <el-tag v-else-if="props.buildingInfo?.buildingType === '2'" class="female-tag" size="small">仅女学生</el-tag>
+            <el-tag v-else-if="props.buildingInfo?.buildingType === '3'" type="info" size="small">男女混合</el-tag>
           </h3>
           <div class="stats">
             已选择 <el-tag type="warning" size="small">{{ selectedStudents.length }}</el-tag> 名学生
@@ -92,17 +178,19 @@
             ref="studentTableRef"
             :data="filteredStudentList"
             row-key="id"
-            height="400"
+            height="330"
+            max-height="330"
             @selection-change="handleStudentSelectionChange"
             @select-all="handleStudentSelectAll"
           >
             <el-table-column type="selection" width="55" />
+            <el-table-column type="index" label="序号" width="60" align="center" />
             <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>
+                <el-tag v-if="scope.row.gender === '0'" type="primary" size="small">男</el-tag>
+                <el-tag v-else-if="scope.row.gender === '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" />
@@ -114,7 +202,9 @@
     <template #footer>
       <div class="dialog-footer">
         <el-button @click="handleClose">取消</el-button>
-        <el-button type="primary" @click="handleConfirm" :disabled="!isMatched">确认分配</el-button>
+        <el-button type="primary" @click="handleConfirm" :disabled="!isMatched">
+          确认{{ allocationType === 'random' ? '随机' : '按学号' }}分配
+        </el-button>
       </div>
     </template>
   </el-dialog>
@@ -122,13 +212,17 @@
 
 <script setup lang="ts">
 import { ref, computed, watch, nextTick } from 'vue';
-import { House, User, Connection } from '@element-plus/icons-vue';
+import { House, User, Connection, Avatar } from '@element-plus/icons-vue';
 import type { DormBuildingTreeVO } from '@/api/xg/xgss/dormBedStudent/dormBedStudent/types';
 import {
-  getClassTeacherBedDetails,
   getClassTeacherUnallocatedStudents,
-  classTeacherSelectiveAllocation
+  classTeacherSelectiveAllocation,
+  getClassTeacherDormTree
 } 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';
 import ElMessage from 'element-plus/es/components/message/index';
 
 interface Props {
@@ -136,6 +230,7 @@ interface Props {
   title?: string;
   buildingInfo?: DormBuildingTreeVO;
   classId?: string | number;
+  parentClassInfo?: any; // 父组件传递的班级信息对象
 }
 
 interface Emits {
@@ -159,6 +254,12 @@ const dialogVisible = computed({
 const bedLoading = ref(false);
 const studentLoading = ref(false);
 
+// 添加请求取消控制器 - 统一命名为 currentController
+let currentController: AbortController | null = null;
+
+// 小延迟,确保加载动画先渲染
+const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
+
 // 搜索关键字
 const bedSearchKey = ref('');
 const studentSearchKey = ref('');
@@ -167,28 +268,83 @@ const studentSearchKey = ref('');
 const bedTableRef = ref();
 const studentTableRef = ref();
 
-// 数据列表
-const bedList = ref<any[]>([]);
+// 数据列表 - 使用与主页面一致的命名
+const roomList = ref<any[]>([]);
 const studentList = ref<any[]>([]);
 const selectedBeds = ref<any[]>([]);
 const selectedStudents = ref<any[]>([]);
 
+// 展开行键值 - 使用与主页面一致的命名
+const expandRowKeys = ref<string[]>([]);
+
+// 班级信息和宿舍树
+const classInfo = ref<any>(null);
+const dormTree = ref<DormBuildingTreeVO[]>([]);
+
+// 添加选择状态管理
+const internalSelection = ref<any[]>([]);
+const isUpdatingSelection = ref(false); // 防止循环调用的标志
+
 // 计算属性:过滤后的列表
 const filteredBedList = computed(() => {
-  if (!bedSearchKey.value) return bedList.value;
+  if (!bedSearchKey.value) return roomList.value;
   const key = bedSearchKey.value.toLowerCase();
-  return bedList.value.filter((bed) => bed.roomName.toLowerCase().includes(key) || bed.bedName.toLowerCase().includes(key));
+
+  const filtered = roomList.value
+    .filter((room) => {
+      // 检查房间名称是否匹配
+      const roomMatches = room.roomName.toLowerCase().includes(key);
+
+      // 检查房间下的床位是否有匹配的
+      const bedMatches = room.children?.some((bed: any) => bed.bedName?.toLowerCase().includes(key));
+
+      return roomMatches || bedMatches;
+    })
+    .map((room) => {
+      // 如果是房间名称匹配,返回完整房间数据
+      if (room.roomName.toLowerCase().includes(key)) {
+        return room;
+      }
+
+      // 如果是床位匹配,只返回匹配的床位
+      return {
+        ...room,
+        children: room.children?.filter((bed: any) => bed.bedName?.toLowerCase().includes(key))
+      };
+    });
+
+  return filtered;
 });
 
 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));
+  let filteredList = studentList.value;
+
+  // 根据楼栋类型过滤学生性别
+  if (props.buildingInfo?.buildingType) {
+    const buildingType = props.buildingInfo.buildingType;
+
+    if (buildingType === '1') {
+      // 男生楼,只显示男学生(gender === '0')
+      filteredList = filteredList.filter((student) => student.gender === '0');
+    } else if (buildingType === '2') {
+      // 女生楼,只显示女学生(gender === '1')
+      filteredList = filteredList.filter((student) => student.gender === '1');
+    }
+    // 混合楼(buildingType === '3')显示所有学生,不需要过滤
+  }
+
+  // 搜索关键字过滤
+  if (studentSearchKey.value) {
+    const key = studentSearchKey.value.toLowerCase();
+    filteredList = filteredList.filter((student) => student.studentNo.toLowerCase().includes(key) || student.studentName.toLowerCase().includes(key));
+  }
+
+  return filteredList;
 });
 
 // 计算属性:是否匹配
 const isMatched = computed(() => {
-  return selectedBeds.value.length > 0 && selectedStudents.value.length > 0 && selectedBeds.value.length === selectedStudents.value.length;
+  return selectedBeds.value.length > 0 && selectedStudents.value.length > 0 && selectedBeds.value.length >= selectedStudents.value.length;
 });
 
 // 监听建筑信息变化,加载数据
@@ -212,39 +368,160 @@ watch(
   }
 );
 
+// 添加分配方式状态
+const allocationType = ref<'random' | 'studentNo'>('random');
+
 /** 加载数据 */
 const loadData = async () => {
-  await Promise.all([loadBedData(), loadStudentData()]);
+  // 取消之前未完成的请求
+  if (currentController) {
+    currentController.abort();
+    currentController = null;
+  }
+
+  await Promise.all([loadClassInfo(), loadRoomData(), loadStudentData()]);
 };
 
-/** 加载床位数据 */
-const loadBedData = async () => {
-  if (!props.buildingInfo) return;
+/** 加载房间数据 - 完全参考主页面的 loadRoomData 逻辑 */
+const loadRoomData = async () => {
+  if (!props.buildingInfo || !props.classId) return;
+
+  // 取消之前未完成的请求
+  if (currentController) {
+    currentController.abort();
+    currentController = null;
+  }
+
+  // 创建新的AbortController
+  currentController = new AbortController();
+  const controller = currentController;
 
   bedLoading.value = true;
+
   try {
-    const res = await getClassTeacherBedDetails(props.buildingInfo.id, 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({
+    // 构建查询参数 - 参考主页面的查询参数结构
+    const query = {
+      buildingId: props.buildingInfo.id,
+      pageNum: 1,
+      pageSize: 10000
+    };
+
+    // 获取指定班级的宿舍房间信息列表
+    const res = await listDormRoomByClassId(props.classId, query);
+    const rooms = res.rows || [];
+
+    // 如果请求已被取消,不继续处理
+    if (controller?.signal?.aborted) return;
+
+    // 并行获取所有房间的床位详细信息 - 完全参考主页面的处理逻辑
+    const roomPromises = rooms.map(async (room) => {
+      // 创建房间记录
+      const roomRecord = {
+        ...room,
+        isBed: false,
+        children: []
+      };
+
+      try {
+        // 调用与主页面相同的接口获取指定房间和班级的床位详细信息
+        const bedRes = await getBedDetailsByRoomIdAndClassId(room.id, props.classId as any);
+
+        // 如果请求已被取消,不继续处理
+        if (controller?.signal?.aborted) return roomRecord;
+
+        const beds = bedRes.data || [];
+
+        // 将床位信息转换为符合表格展示的格式,并过滤只显示空闲床位
+        if (beds.length > 0) {
+          // 过滤只显示空闲床位(没有学生分配且不是维修中)
+          const availableBeds = beds
+            .filter((bed: any) => !bed.studentId && bed.bedStatus !== '2')
+            .map((bed: any) => ({
               ...bed,
-              roomName: room.roomName
-            });
+              isBed: true,
+              roomName: room.roomName, // 保留房间名称,方便展示
+              // 确保学生相关字段存在,即使是空闲床位
+              studentName: bed.studentName || null,
+              studentNo: bed.studentNo || null,
+              studentGender: bed.studentGender || bed.gender || null
+            }));
+
+          // 只有当房间有可用床位时才设置children
+          if (availableBeds.length > 0) {
+            roomRecord.children = availableBeds;
           }
-        });
+        }
+      } catch (error: any) {
+        // 忽略请求被取消的错误
+        if (error.name !== 'AbortError') {
+          console.error(`获取房间 ${room.id} 的床位详细信息失败`, error);
+        }
       }
+
+      return roomRecord;
     });
-    bedList.value = allBeds;
-  } catch (error) {
-    console.error('获取床位数据失败', error);
-    ElMessage.error('获取床位数据失败');
+
+    // 等待所有请求完成
+    const tempList = await Promise.all(roomPromises);
+
+    // 如果请求已被取消,不更新UI
+    if (controller?.signal?.aborted) return;
+
+    // 过滤掉没有可用床位的房间记录
+    const finalRoomList = tempList.filter((room) => room.children && room.children.length > 0);
+
+    roomList.value = finalRoomList;
+
+    console.log('弹窗获取到的房间和床位数据:', finalRoomList);
+
+    // 设置展开行键值,确保树形结构正确展开 - 参考主页面的展开逻辑
+    expandRowKeys.value = finalRoomList.map((room) => String(room.id));
+
+    // 强制更新表格,确保树形结构正确渲染
+    nextTick(() => {
+      if (bedTableRef.value) {
+        console.log('表格引用存在,尝试展开所有节点');
+        console.log('展开行键值:', expandRowKeys.value);
+        // 手动触发表格重新渲染
+        bedTableRef.value.doLayout();
+      }
+    });
+  } catch (error: any) {
+    // 忽略请求被取消的错误
+    if (error.name !== 'AbortError') {
+      console.error('获取床位数据失败', error);
+      ElMessage.error('获取床位数据失败');
+    }
   } finally {
-    bedLoading.value = false;
+    if (controller === currentController) {
+      bedLoading.value = false;
+    }
+  }
+};
+
+/** 获取行类名,用于区分房间和床位 */
+const getRowClassName = ({ row }: { row: any }) => {
+  if (!row.isBed) {
+    return 'room-row';
+  }
+  return 'bed-row';
+};
+
+/** 加载班级信息 */
+const loadClassInfo = async () => {
+  // 优先使用父组件传递的班级信息
+  if (props.parentClassInfo) {
+    classInfo.value = props.parentClassInfo;
+    return;
+  }
+
+  // 如果没有传递班级信息,则通过API获取
+  try {
+    const res = await getMyClassListAsTeacher();
+    const classList = res.data || [];
+    classInfo.value = classList.find((cls: any) => cls.id == props.classId);
+  } catch (error) {
+    console.error('获取班级信息失败', error);
   }
 };
 
@@ -253,7 +530,13 @@ const loadStudentData = async () => {
   studentLoading.value = true;
   try {
     const res = await getClassTeacherUnallocatedStudents(props.classId as any);
-    studentList.value = res.data || [];
+    const students = res.data || [];
+
+    // 为学生数据添加班级名称
+    studentList.value = students.map((student: any) => ({
+      ...student,
+      className: classInfo.value?.className || '未知班级'
+    }));
   } catch (error) {
     console.error('获取学生数据失败', error);
     ElMessage.error('获取学生数据失败');
@@ -267,6 +550,19 @@ const handleBedSearch = () => {
   // 实时搜索,computed 已处理
 };
 
+/** 处理行的展开/收起 */
+const handleExpandChange = (row: any, expanded: boolean) => {
+  const expandKey = String(row.id);
+  if (expanded) {
+    expandRowKeys.value.push(expandKey);
+  } else {
+    const index = expandRowKeys.value.findIndex((key) => key === expandKey);
+    if (index !== -1) {
+      expandRowKeys.value.splice(index, 1);
+    }
+  }
+};
+
 /** 学生搜索 */
 const handleStudentSearch = () => {
   // 实时搜索,computed 已处理
@@ -274,17 +570,98 @@ const handleStudentSearch = () => {
 
 /** 检查床位是否可选 */
 const checkBedSelectable = (row: any) => {
+  // 房间节点:当房间下有空闲床位时可选
+  if (!row.isBed) {
+    return row.children && row.children.length > 0;
+  }
+  // 床位节点:空闲床位可选
   return !row.studentId && row.bedStatus !== '2';
 };
 
 /** 床位选择变化 */
 const handleBedSelectionChange = (selection: any[]) => {
-  selectedBeds.value = selection;
+  if (isUpdatingSelection.value) return; // 防止循环调用
+
+  console.log('选择变化:', selection);
+
+  // 只更新床位选择,实际的房间联动在handleRowSelection中处理
+  selectedBeds.value = selection.filter((item) => item.isBed);
+};
+
+/** 处理单行选择 */
+const handleRowSelection = (selection: any[], row: any) => {
+  if (isUpdatingSelection.value) return; // 防止循环调用
+
+  console.log(
+    '单行选择:',
+    row,
+    '是否选中:',
+    selection.find((item) => item.id === row.id)
+  );
+
+  const isSelected = selection.find((item) => item.id === row.id);
+
+  if (!row.isBed) {
+    // 处理房间选择
+    isUpdatingSelection.value = true;
+
+    if (isSelected) {
+      // 选择房间时,自动选中该房间下的所有空闲床位
+      const roomBeds = row.children?.filter((bed: any) => checkBedSelectable(bed)) || [];
+
+      roomBeds.forEach((bed) => {
+        const bedAlreadySelected = selection.find((item) => item.id === bed.id);
+        if (!bedAlreadySelected) {
+          bedTableRef.value?.toggleRowSelection(bed, true);
+        }
+      });
+    } else {
+      // 取消选择房间时,取消该房间下所有床位的选择
+      const roomBeds = row.children?.filter((bed: any) => checkBedSelectable(bed)) || [];
+
+      roomBeds.forEach((bed) => {
+        bedTableRef.value?.toggleRowSelection(bed, false);
+      });
+    }
+
+    nextTick(() => {
+      isUpdatingSelection.value = false;
+    });
+  }
+
+  // 更新最终的床位选择(延迟更新,确保房间联动完成)
+  nextTick(() => {
+    if (!isUpdatingSelection.value) {
+      selectedBeds.value = selection.filter((item) => item.isBed);
+    }
+  });
 };
 
 /** 床位全选 */
 const handleBedSelectAll = (selection: any[]) => {
-  selectedBeds.value = selection;
+  // 全选时,选择所有可选的房间和床位
+  if (selection.length > 0) {
+    const allSelectable = [];
+
+    // 遍历所有房间,添加可选的房间和床位
+    roomList.value.forEach((room) => {
+      if (checkBedSelectable(room)) {
+        allSelectable.push(room);
+      }
+      if (room.children) {
+        room.children.forEach((bed: any) => {
+          if (checkBedSelectable(bed)) {
+            allSelectable.push(bed);
+          }
+        });
+      }
+    });
+
+    handleBedSelectionChange(allSelectable);
+  } else {
+    // 取消全选
+    selectedBeds.value = [];
+  }
 };
 
 /** 学生选择变化 */
@@ -297,32 +674,10 @@ 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 () => {
+/** 确认分配 */
+const handleConfirm = async () => {
   if (!isMatched.value) {
-    ElMessage.warning('请确保床位和学生数量匹配');
+    ElMessage.warning('请确保床位数量大于等于学生数量');
     return;
   }
 
@@ -330,23 +685,17 @@ const handleStudentNoAllocation = async () => {
     const allocationParams = {
       bedIds: selectedBeds.value.map((bed) => bed.id),
       studentIds: selectedStudents.value.map((student) => student.id),
-      allocationType: 'studentNo' as const
+      allocationType: allocationType.value
     };
 
     await classTeacherSelectiveAllocation(allocationParams, props.classId as any);
-    ElMessage.success('按学号分配成功');
+    ElMessage.success('分配成功');
     handleSuccess();
   } catch (error) {
-    ElMessage.error('按学号分配失败');
+    ElMessage.error('分配失败');
   }
 };
 
-/** 确认分配 */
-const handleConfirm = () => {
-  // 默认使用随机分配
-  handleRandomAllocation();
-};
-
 /** 分配成功处理 */
 const handleSuccess = () => {
   emit('success');
@@ -355,13 +704,20 @@ const handleSuccess = () => {
 
 /** 关闭对话框 */
 const handleClose = () => {
+  // 取消所有未完成的请求
+  if (currentController) {
+    currentController.abort();
+    currentController = null;
+  }
+
   // 重置数据
   selectedBeds.value = [];
   selectedStudents.value = [];
   bedSearchKey.value = '';
   studentSearchKey.value = '';
-  bedList.value = [];
+  roomList.value = [];
   studentList.value = [];
+  allocationType.value = 'random';
 
   // 清除表格选择
   nextTick(() => {
@@ -381,7 +737,8 @@ const handleClose = () => {
 .allocation-container {
   display: flex;
   gap: 20px;
-  height: 500px;
+  height: 480px;
+  width: 100%;
 }
 
 .selection-area {
@@ -391,6 +748,7 @@ const handleClose = () => {
   border: 1px solid #e4e7ed;
   border-radius: 8px;
   overflow: hidden;
+  min-width: 0;
 }
 
 .area-header {
@@ -433,12 +791,14 @@ const handleClose = () => {
   width: 200px;
   display: flex;
   flex-direction: column;
-  justify-content: center;
+  justify-content: space-between;
   align-items: center;
-  gap: 30px;
-  padding: 20px;
+  gap: 15px;
+  padding: 15px;
   background: #fafafa;
   border-radius: 8px;
+  height: 100%;
+  box-sizing: border-box;
 }
 
 .match-info {
@@ -446,13 +806,13 @@ const handleClose = () => {
 }
 
 .match-icon {
-  font-size: 32px;
+  font-size: 28px;
   color: #909399;
-  margin-bottom: 15px;
+  margin-bottom: 10px;
 }
 
 .match-stats {
-  margin-bottom: 15px;
+  margin-bottom: 10px;
 }
 
 .stat-item {
@@ -494,12 +854,42 @@ const handleClose = () => {
 .allocation-buttons {
   display: flex;
   flex-direction: column;
-  gap: 15px;
+  gap: 12px;
   width: 100%;
+  align-items: stretch;
+  flex-shrink: 0;
+}
+
+.allocation-options {
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  padding: 12px;
+  text-align: center;
+  flex-shrink: 0;
+}
+
+.option-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 8px;
+}
+
+.allocation-options :deep(.el-radio-group) {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.allocation-options :deep(.el-radio) {
+  margin-right: 0;
+  margin-bottom: 0;
 }
 
 .allocation-buttons .el-button {
   width: 100%;
+  margin: 0;
 }
 
 .dialog-footer {
@@ -512,7 +902,7 @@ const handleClose = () => {
   border-color: rgba(255, 105, 180, 0.1) !important;
 }
 
-/* 表格样式优化 */
+/* 表格样式优化 - 参考主页面样式 */
 .selection-area :deep(.el-table) {
   border: none;
 }
@@ -534,4 +924,204 @@ const handleClose = () => {
 .selection-area :deep(.el-table__row:hover > td) {
   background-color: #f5f7fa !important;
 }
+
+/* 确保表格内容不换行 */
+.selection-area :deep(.el-table .cell) {
+  white-space: nowrap;
+}
+
+/* 表格列宽度自适应 */
+.selection-area :deep(.el-table) {
+  table-layout: auto;
+}
+
+/* 房间和床位行样式区分 - 参考主页面 */
+.selection-area :deep(.room-row) {
+  background-color: #eefeff !important;
+}
+
+.selection-area :deep(.bed-row) {
+  background-color: #ffffff !important;
+}
+
+.selection-area :deep(.room-row:hover) {
+  background-color: #d1f0ff !important;
+}
+
+.selection-area :deep(.bed-row:hover) {
+  background-color: #e8f0f5 !important;
+}
+
+/* 表格边框和分割线样式 */
+.selection-area :deep(.el-table) {
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.selection-area :deep(.el-table__inner-wrapper) {
+  border-radius: 8px;
+}
+
+/* 表头样式优化 */
+.selection-area :deep(.el-table__header-wrapper) {
+  border-radius: 8px 8px 0 0;
+}
+
+/* 表格行分割线 */
+.selection-area :deep(.el-table td) {
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.selection-area :deep(.el-table tr:last-child td) {
+  border-bottom: none;
+}
+
+/* 表格选择列样式优化 */
+.selection-area :deep(.el-table .el-table__cell.is-center) {
+  text-align: center;
+}
+
+/* 表格内容垂直居中 */
+.selection-area :deep(.el-table .cell) {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 40px;
+}
+
+/* 房间和床位名称列左对齐 - 加强选择器优先级 */
+.selection-area :deep(.el-table .el-table__cell:first-child .cell) {
+  justify-content: flex-start !important;
+  align-items: center !important;
+}
+
+/* 树形结构样式 */
+.room-node {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-weight: 600;
+  color: #303133;
+  padding: 4px 0;
+  width: 100%;
+}
+
+.room-name {
+  margin-right: 8px;
+  font-size: 14px;
+}
+
+.bed-node {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: #606266;
+  padding: 4px 0;
+  width: 100%;
+}
+
+.bed-name {
+  font-size: 13px;
+}
+
+/* 图标样式优化 */
+.room-node .el-icon,
+.bed-node .el-icon {
+  font-size: 16px;
+}
+
+.room-node .el-icon {
+  /* color: #409eff; */
+}
+
+.bed-node .el-icon {
+  /* color: #67c23a; */
+}
+
+/* 性别标签样式 - 与主页面保持一致 */
+.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-tag--success) {
+  background-color: rgba(103, 194, 58, 0.1) !important;
+  color: #67c23a !important;
+  border-color: rgba(103, 194, 58, 0.1) !important;
+}
+
+.selection-area :deep(.el-tag--warning) {
+  background-color: rgba(230, 162, 60, 0.1) !important;
+  color: #e6a23c !important;
+  border-color: rgba(230, 162, 60, 0.1) !important;
+}
+
+.selection-area :deep(.el-tag--danger) {
+  background-color: rgba(245, 108, 108, 0.1) !important;
+  color: #f56c6c !important;
+  border-color: rgba(245, 108, 108, 0.1) !important;
+}
+
+.selection-area :deep(.el-tag--info) {
+  background-color: rgba(144, 147, 153, 0.1) !important;
+  color: #909399 !important;
+  border-color: rgba(144, 147, 153, 0.1) !important;
+}
+
+/* 分配信息区域样式 */
+.allocation-info {
+  text-align: center;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  padding: 12px;
+  width: 100%;
+  flex-shrink: 0;
+}
+
+.info-header {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  margin-bottom: 8px;
+  color: #303133;
+}
+
+.info-icon {
+  font-size: 18px;
+  color: #409eff;
+}
+
+.info-title {
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.info-content {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 13px;
+  padding: 2px 0;
+}
+
+.info-label {
+  color: #606266;
+  font-weight: 500;
+}
+
+.info-value {
+  color: #303133;
+  font-weight: 600;
+}
 </style>

+ 11 - 5
web/src/views/xg/xgss/ejxyfp/ejxyfp.vue

@@ -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
web/src/views/xg/xgss/xsssfpejxy/index.vue

@@ -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';