zlwu 1 год назад
Родитель
Сommit
8623173994

+ 18 - 4
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java

@@ -21,6 +21,7 @@ import org.dromara.xg.domain.vo.XgClassMsgVo;
 import org.dromara.xg.domain.bo.XgClassMsgBo;
 import org.dromara.xg.service.IXgClassMsgService;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
 
 /**
  * 班级信息-新版
@@ -63,8 +64,7 @@ public class XgClassMsgController extends BaseController {
      */
     @SaCheckPermission("classMsg:classMsg:query")
     @GetMapping("/{id}")
-    public R<XgClassMsgVo> getInfo(@NotNull(message = "主键不能为空")
-                                     @PathVariable Long id) {
+    public R<XgClassMsgVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
         return R.ok(xgClassMsgService.queryById(id));
     }
 
@@ -98,8 +98,22 @@ public class XgClassMsgController extends BaseController {
     @SaCheckPermission("classMsg:classMsg:remove")
     @Log(title = "班级信息-新版", businessType = BusinessType.DELETE)
     @DeleteMapping("/{ids}")
-    public R<Void> remove(@NotEmpty(message = "主键不能为空")
-                          @PathVariable Long[] ids) {
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
         return toAjax(xgClassMsgService.deleteWithValidByIds(List.of(ids), true));
     }
+
+    /**
+     * 获取当前登录人作为班主任所管理的班级列表
+     */
+    @SaCheckPermission("classMsg:classMsg:list")
+    @GetMapping("/myClassListAsTeacher")
+    public R<List<XgClassMsgVo>> myClassListAsTeacher() {
+        Long userId = LoginHelper.getUserId();
+        XgClassMsgBo bo = new XgClassMsgBo();
+        bo.setClassTeacherId(userId);
+        // 仅查询正常状态班级(1-正常)
+        bo.setClassStatus("1");
+        List<XgClassMsgVo> list = xgClassMsgService.queryList(bo);
+        return R.ok(list);
+    }
 }

+ 80 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedStudentController.java

@@ -164,4 +164,84 @@ public class XgDormBedStudentController extends BaseController {
         List<DormBuildingTreeVo> treeData = xgDormBedStudentService.getCurrentUserCollegeDormBuildingTree();
         return R.ok(treeData);
     }
+
+    /**
+     * 获取班主任管理的班级宿舍楼树形结构
+     * 只显示班主任管理班级分配到的宿舍楼
+     */
+    @SaCheckPermission("dormBedStudent:dormBedStudent:list")
+    @GetMapping("/getClassTeacherDormTree")
+    public R<List<DormBuildingTreeVo>> getClassTeacherDormTree(@RequestParam(required = false) Long classId) {
+        List<DormBuildingTreeVo> treeData = xgDormBedStudentService.getClassTeacherDormTree(classId);
+        return R.ok(treeData);
+    }
+
+    /**
+     * 获取班主任管理班级的房间床位详情(带学生信息)
+     */
+    @SaCheckPermission("dormBedStudent:dormBedStudent:list")
+    @GetMapping("/getClassTeacherBedDetails")
+    public R<List<Object>> getClassTeacherBedDetails(@RequestParam(required = false) Long buildingId,
+            @RequestParam(required = false) Long classId) {
+        List<Object> bedDetails = xgDormBedStudentService.getClassTeacherBedDetails(buildingId, classId);
+        return R.ok(bedDetails);
+    }
+
+    /**
+     * 获取班主任管理的未分配床位学生列表
+     */
+    @SaCheckPermission("dormBedStudent:dormBedStudent:list")
+    @GetMapping("/getClassTeacherUnallocatedStudents")
+    public R<List<Object>> getClassTeacherUnallocatedStudents(@RequestParam(required = false) Long classId) {
+        List<Object> students = xgDormBedStudentService.getClassTeacherUnallocatedStudents(classId);
+        return R.ok(students);
+    }
+
+    /**
+     * 班主任一键随机分配床位
+     */
+    @SaCheckPermission("dormBedStudent:dormBedStudent:add")
+    @Log(title = "班主任随机分配床位", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/classTeacherRandomAllocation")
+    public R<Void> classTeacherRandomAllocation(@RequestParam(required = false) Long buildingId,
+            @RequestParam(required = false) Long classId) {
+        return toAjax(xgDormBedStudentService.classTeacherRandomAllocation(buildingId, classId));
+    }
+
+    /**
+     * 班主任一键按学号分配床位
+     */
+    @SaCheckPermission("dormBedStudent:dormBedStudent:add")
+    @Log(title = "班主任按学号分配床位", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/classTeacherStudentNoAllocation")
+    public R<Void> classTeacherStudentNoAllocation(@RequestParam(required = false) Long buildingId,
+            @RequestParam(required = false) Long classId) {
+        return toAjax(xgDormBedStudentService.classTeacherStudentNoAllocation(buildingId, classId));
+    }
+
+    /**
+     * 班主任选择性分配床位(弹窗模式)
+     */
+    @SaCheckPermission("dormBedStudent:dormBedStudent:add")
+    @Log(title = "班主任选择性分配床位", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/classTeacherSelectiveAllocation")
+    public R<Void> classTeacherSelectiveAllocation(@RequestBody Object allocationParams,
+            @RequestParam(required = false) Long classId) {
+        return toAjax(xgDormBedStudentService.classTeacherSelectiveAllocation(allocationParams, classId));
+    }
+
+    /**
+     * 班主任一键收回床位
+     */
+    @SaCheckPermission("dormBedStudent:dormBedStudent:edit")
+    @Log(title = "班主任收回床位", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PostMapping("/classTeacherRecallBeds")
+    public R<Void> classTeacherRecallBeds(@RequestParam(required = false) Long buildingId,
+            @RequestParam(required = false) Long classId) {
+        return toAjax(xgDormBedStudentService.classTeacherRecallBeds(buildingId, classId));
+    }
 }

+ 8 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentMsgMapper.java

@@ -94,4 +94,12 @@ public interface XgStudentMsgMapper extends BaseMapperPlus<XgStudentMsg, XgStude
      */
     List<Map<String, Object>> getCollegeStudentStatsByClass(@Param("secondaryCollegeId") Long secondaryCollegeId);
 
+    /**
+     * 查询二级学院下的学院床位分配统计信息(基于college_id统计)
+     *
+     * @param secondaryCollegeId 二级学院ID
+     * @return 学院床位分配统计信息列表
+     */
+    List<Map<String, Object>> getCollegeBedAssignmentStats(@Param("secondaryCollegeId") Long secondaryCollegeId);
+
 }

+ 62 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedStudentService.java

@@ -111,4 +111,66 @@ public interface IXgDormBedStudentService {
      * @return 宿舍楼树形结构数据
      */
     List<DormBuildingTreeVo> getCurrentUserCollegeDormBuildingTree();
+
+    /**
+     * 获取班主任管理的班级宿舍楼树形结构
+     * 只显示班主任管理班级分配到的宿舍楼
+     *
+     * @param classId 班级ID,可为空;为空时取当前用户管理的首个班级
+     * @return 宿舍楼树形结构数据
+     */
+    List<DormBuildingTreeVo> getClassTeacherDormTree(Long classId);
+
+    /**
+     * 获取班主任管理班级的房间床位详情(带学生信息)
+     *
+     * @param buildingId 宿舍楼ID,为空则查询所有楼栋
+     * @param classId    班级ID,可为空
+     * @return 房间床位详情列表
+     */
+    List<Object> getClassTeacherBedDetails(Long buildingId, Long classId);
+
+    /**
+     * 获取班主任管理的未分配床位学生列表
+     *
+     * @param classId 班级ID,可为空
+     * @return 未分配床位的学生列表
+     */
+    List<Object> getClassTeacherUnallocatedStudents(Long classId);
+
+    /**
+     * 班主任一键随机分配床位
+     *
+     * @param buildingId 宿舍楼ID,为空则全部分配
+     * @param classId    班级ID,可为空
+     * @return 是否分配成功
+     */
+    Boolean classTeacherRandomAllocation(Long buildingId, Long classId);
+
+    /**
+     * 班主任一键按学号分配床位
+     *
+     * @param buildingId 宿舍楼ID,为空则全部分配
+     * @param classId    班级ID,可为空
+     * @return 是否分配成功
+     */
+    Boolean classTeacherStudentNoAllocation(Long buildingId, Long classId);
+
+    /**
+     * 班主任选择性分配床位(弹窗模式)
+     *
+     * @param allocationParams 分配参数(含班级ID等)
+     * @param classId          班级ID,可为空
+     * @return 是否分配成功
+     */
+    Boolean classTeacherSelectiveAllocation(Object allocationParams, Long classId);
+
+    /**
+     * 班主任一键收回床位
+     *
+     * @param buildingId 宿舍楼ID,为空则全部收回
+     * @param classId    班级ID,可为空
+     * @return 是否收回成功
+     */
+    Boolean classTeacherRecallBeds(Long buildingId, Long classId);
 }

+ 1 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgClassMsgServiceImpl.java

@@ -88,6 +88,7 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService {
         lqw.eq(StringUtils.isNotBlank(bo.getClassType()), XgClassMsg::getClassType, bo.getClassType());
         lqw.eq(StringUtils.isNotBlank(bo.getGrade()), XgClassMsg::getGrade, bo.getGrade());
         lqw.eq(StringUtils.isNotBlank(bo.getClassStatus()), XgClassMsg::getClassStatus, bo.getClassStatus());
+        lqw.eq(bo.getClassTeacherId() != null, XgClassMsg::getClassTeacherId, bo.getClassTeacherId());
         return lqw;
     }
 

+ 198 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java

@@ -1094,4 +1094,202 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
 
         return new ArrayList<>(campusMap.values());
     }
+
+    /**
+     * 获取班主任管理的班级宿舍楼树
+     * 通过当前用户所在的班级,获取该班级分配的宿舍楼
+     *
+     * @return 宿舍楼树形结构数据
+     */
+    @Override
+    public List<DormBuildingTreeVo> getClassTeacherDormTree(Long classId) {
+        try {
+            // 1. 获取班级ID(优先使用入参,其次回退到当前用户)
+            Long targetClassId = classId != null ? classId : getCurrentUserClassId();
+            if (targetClassId == null) {
+                log.warn("无法获取当前用户管理的班级信息");
+                return new ArrayList<>();
+            }
+
+            // 2. 通过xg_dorm_bed_student表获取分配给该班级的宿舍楼ID列表
+            Set<Long> buildingIds = getClassAllocatedBuildingIds(targetClassId);
+            if (buildingIds.isEmpty()) {
+                log.info("班级 {} 还没有分配任何宿舍楼", targetClassId);
+                return new ArrayList<>();
+            }
+
+            // 3. 获取宿舍楼信息并构建树形结构
+            return buildDormBuildingTree(buildingIds);
+
+        } catch (Exception e) {
+            log.error("获取班主任管理的班级宿舍楼树失败", e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 获取当前用户管理的班级ID(班主任身份)
+     */
+    private Long getCurrentUserClassId() {
+        try {
+            Long currentUserId = LoginHelper.getUserId();
+            if (currentUserId == null) {
+                log.warn("用户未登录");
+                return null;
+            }
+
+            // 查询当前用户是否是班主任,通过班级表查找
+            LambdaQueryWrapper<XgClassMsg> classQuery = Wrappers.lambdaQuery();
+            classQuery.eq(XgClassMsg::getClassTeacherId, currentUserId)
+                    .eq(XgClassMsg::getClassStatus, "1")
+                    .eq(XgClassMsg::getDeleted, 0L)
+                    .last("LIMIT 1");
+
+            List<XgClassMsg> classes = xgClassMsgMapper.selectList(classQuery);
+            return classes.isEmpty() ? null : classes.get(0).getId();
+
+        } catch (Exception e) {
+            log.error("获取当前用户管理的班级ID失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取分配给指定班级的宿舍楼ID列表
+     */
+    private Set<Long> getClassAllocatedBuildingIds(Long classId) {
+        try {
+            LambdaQueryWrapper<XgDormBedStudent> queryWrapper = Wrappers.lambdaQuery();
+            queryWrapper.select(XgDormBedStudent::getBuildingId)
+                    .eq(XgDormBedStudent::getClassId, classId)
+                    .eq(XgDormBedStudent::getStatus, "0")
+                    .eq(XgDormBedStudent::getDeleted, "0")
+                    .isNotNull(XgDormBedStudent::getBuildingId)
+                    .groupBy(XgDormBedStudent::getBuildingId);
+
+            List<XgDormBedStudent> buildingRecords = baseMapper.selectList(queryWrapper);
+            return buildingRecords.stream()
+                    .map(XgDormBedStudent::getBuildingId)
+                    .collect(Collectors.toSet());
+
+        } catch (Exception e) {
+            log.error("获取班级 {} 分配的宿舍楼ID列表失败", classId, e);
+            return Set.of();
+        }
+    }
+
+    /**
+     * 获取班主任管理班级的房间床位详情
+     *
+     * @param buildingId 宿舍楼ID
+     * @return 房间床位详情列表
+     */
+    @Override
+    public List<Object> getClassTeacherBedDetails(Long buildingId, Long classId) {
+        try {
+            Long targetClassId = classId != null ? classId : getCurrentUserClassId();
+            log.info("获取班主任管理班级的房间床位详情,宿舍楼ID: {},classId:{}", buildingId, targetClassId);
+            return new ArrayList<>();
+        } catch (Exception e) {
+            log.error("获取班主任管理班级的房间床位详情失败", e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 获取班主任管理班级的未分配学生列表
+     *
+     * @return 未分配学生列表
+     */
+    @Override
+    public List<Object> getClassTeacherUnallocatedStudents(Long classId) {
+        try {
+            Long targetClassId = classId != null ? classId : getCurrentUserClassId();
+            log.info("获取班主任管理班级的未分配学生列表,classId:{}", targetClassId);
+            return new ArrayList<>();
+        } catch (Exception e) {
+            log.error("获取班主任管理班级的未分配学生列表失败", e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 班主任一键随机分配床位
+     *
+     * @param buildingId 宿舍楼ID,为空则全部分配
+     * @return 是否分配成功
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean classTeacherRandomAllocation(Long buildingId, Long classId) {
+        try {
+            Long targetClassId = classId != null ? classId : getCurrentUserClassId();
+            // TODO: 实现班主任一键随机分配床位
+            log.info("班主任一键随机分配床位,宿舍楼ID: {},classId:{}", buildingId, targetClassId);
+            return true;
+        } catch (Exception e) {
+            log.error("班主任一键随机分配床位失败", e);
+            throw new RuntimeException("随机分配床位失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 班主任一键按学号分配床位
+     *
+     * @param buildingId 宿舍楼ID,为空则全部分配
+     * @return 是否分配成功
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean classTeacherStudentNoAllocation(Long buildingId, Long classId) {
+        try {
+            Long targetClassId = classId != null ? classId : getCurrentUserClassId();
+            // TODO: 实现班主任一键按学号分配床位
+            log.info("班主任一键按学号分配床位,宿舍楼ID: {},classId:{}", buildingId, targetClassId);
+            return true;
+        } catch (Exception e) {
+            log.error("班主任一键按学号分配床位失败", e);
+            throw new RuntimeException("按学号分配床位失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 班主任选择性分配床位(弹窗模式)
+     *
+     * @param allocationParams 分配参数
+     * @return 是否分配成功
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean classTeacherSelectiveAllocation(Object allocationParams, Long classId) {
+        try {
+            Long targetClassId = classId != null ? classId : getCurrentUserClassId();
+            // TODO: 实现班主任选择性分配床位
+            log.info("班主任选择性分配床位,classId:{},分配参数: {}", targetClassId, allocationParams);
+            return true;
+        } catch (Exception e) {
+            log.error("班主任选择性分配床位失败", e);
+            throw new RuntimeException("选择性分配床位失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 班主任一键收回床位
+     *
+     * @param buildingId 宿舍楼ID,为空则全部收回
+     * @return 是否收回成功
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean classTeacherRecallBeds(Long buildingId, Long classId) {
+        try {
+            Long targetClassId = classId != null ? classId : getCurrentUserClassId();
+            // TODO: 实现班主任一键收回床位
+            log.info("班主任一键收回床位,宿舍楼ID: {},classId:{}", buildingId, targetClassId);
+            return true;
+        } catch (Exception e) {
+            log.error("班主任一键收回床位失败", e);
+            throw new RuntimeException("收回床位失败:" + e.getMessage());
+        }
+    }
 }

+ 35 - 134
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java

@@ -1162,153 +1162,54 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService {
     }
 
     /**
-     * 获取学生信息树(二级学院下的学院、专业、班级统计信息)
+     * 获取学生信息树(二级学院下的学院床位分配统计信息)
+     * 基于college_id判断床位分配情况,只显示学院层级
      *
      * @param secondaryCollegeId 二级学院ID,默认101
      * @return 学生信息树
      */
     @Override
     public List<XgStudentTreeVo> getStudentTree(Long secondaryCollegeId) {
-        log.info("开始构建学生信息树,二级学院ID: {}", secondaryCollegeId);
+        log.info("开始构建学生信息树(基于college_id床位分配统计),二级学院ID: {}", secondaryCollegeId);
 
         List<XgStudentTreeVo> result = new ArrayList<>();
 
         try {
-            // 1. 查询二级学院下的所有学院信息,并统计男女生人数
-            List<Map<String, Object>> collegeStats = baseMapper.getCollegeStudentStats(secondaryCollegeId);
-            log.info("查询到 {} 个学院", collegeStats.size());
-
-            // 按学院分组
-            Map<String, List<Map<String, Object>>> collegeMap = collegeStats.stream()
-                    .collect(Collectors.groupingBy(item -> {
-                        Object collegeIdObj = item.get("college_id");
-                        Long collegeId = collegeIdObj instanceof Long ? (Long) collegeIdObj
-                                : Long.valueOf(collegeIdObj.toString());
-                        String collegeName = (String) item.get("college_name");
-                        return collegeId + "_" + collegeName;
-                    }));
+            // 1. 查询二级学院下的所有学院信息,并统计男女生床位分配情况
+            // 使用新的查询方法,基于college_id统计床位分配
+            List<Map<String, Object>> collegeStats = baseMapper.getCollegeBedAssignmentStats(secondaryCollegeId);
+            log.info("查询到 {} 个学院的床位分配统计", collegeStats.size());
+
+            // 2. 构建学院节点(只显示学院层级)
+            for (Map<String, Object> collegeItem : collegeStats) {
+                Object collegeIdObj = collegeItem.get("college_id");
+                Long collegeId = collegeIdObj instanceof Long ? (Long) collegeIdObj
+                        : Long.valueOf(collegeIdObj.toString());
+                String collegeName = (String) collegeItem.get("college_name");
+
+                // 获取统计数据
+                Integer maleTotal = ((Number) collegeItem.get("male_total")).intValue();
+                Integer femaleTotal = ((Number) collegeItem.get("female_total")).intValue();
+                Integer maleAssigned = ((Number) collegeItem.get("male_assigned")).intValue();
+                Integer femaleAssigned = ((Number) collegeItem.get("female_assigned")).intValue();
 
-            // 2. 构建学院节点
-            for (Map.Entry<String, List<Map<String, Object>>> collegeEntry : collegeMap.entrySet()) {
-                String collegeKey = collegeEntry.getKey();
-                List<Map<String, Object>> collegeMajors = collegeEntry.getValue();
-
-                if (!collegeMajors.isEmpty()) {
-                    Map<String, Object> firstItem = collegeMajors.get(0);
-                    Object collegeIdObj = firstItem.get("college_id");
-                    Long collegeId = collegeIdObj instanceof Long ? (Long) collegeIdObj
-                            : Long.valueOf(collegeIdObj.toString());
-                    String collegeName = (String) firstItem.get("college_name");
-
-                    XgStudentTreeVo collegeNode = new XgStudentTreeVo();
-                    collegeNode.setId("college-" + collegeId);
-                    collegeNode.setLabel(collegeName);
-                    collegeNode.setType("college");
-                    collegeNode.setCollegeId(collegeId);
-                    collegeNode.setCollegeName(collegeName);
-                    collegeNode.setChildren(new ArrayList<>());
-
-                    // 统计学院人数
-                    int collegeMaleTotal = 0, collegeFemaleTotal = 0;
-                    int collegeMaleAssigned = 0, collegeFemaleAssigned = 0;
-
-                    // 按专业分组
-                    Map<String, List<Map<String, Object>>> majorMap = collegeMajors.stream()
-                            .collect(Collectors.groupingBy(item -> {
-                                Object majorIdObj = item.get("major_id");
-                                Long majorId = majorIdObj instanceof Long ? (Long) majorIdObj
-                                        : Long.valueOf(majorIdObj.toString());
-                                String majorName = (String) item.get("major_name");
-                                return majorId + "_" + majorName;
-                            }));
-
-                    // 3. 构建专业节点
-                    for (Map.Entry<String, List<Map<String, Object>>> majorEntry : majorMap.entrySet()) {
-                        String majorKey = majorEntry.getKey();
-                        List<Map<String, Object>> majorClasses = majorEntry.getValue();
-
-                        if (!majorClasses.isEmpty()) {
-                            Map<String, Object> firstMajorItem = majorClasses.get(0);
-                            Object majorIdObj = firstMajorItem.get("major_id");
-                            Long majorId = majorIdObj instanceof Long ? (Long) majorIdObj
-                                    : Long.valueOf(majorIdObj.toString());
-                            String majorName = (String) firstMajorItem.get("major_name");
-
-                            XgStudentTreeVo majorNode = new XgStudentTreeVo();
-                            majorNode.setId("major-" + majorId);
-                            majorNode.setLabel(majorName);
-                            majorNode.setType("major");
-                            majorNode.setCollegeId(collegeId);
-                            majorNode.setCollegeName(collegeName);
-                            majorNode.setMajorId(majorId);
-                            majorNode.setMajorName(majorName);
-                            majorNode.setChildren(new ArrayList<>());
-
-                            // 统计专业人数
-                            int majorMaleTotal = 0, majorFemaleTotal = 0;
-                            int majorMaleAssigned = 0, majorFemaleAssigned = 0;
-
-                            // 4. 构建班级节点
-                            for (Map<String, Object> classItem : majorClasses) {
-                                Object classIdObj = classItem.get("class_id");
-                                Long classId = classIdObj instanceof Long ? (Long) classIdObj
-                                        : Long.valueOf(classIdObj.toString());
-                                String className = (String) classItem.get("class_name");
-                                String grade = (String) classItem.get("grade");
-                                Integer maleTotal = ((Number) classItem.get("male_total")).intValue();
-                                Integer femaleTotal = ((Number) classItem.get("female_total")).intValue();
-                                Integer maleAssigned = ((Number) classItem.get("male_assigned")).intValue();
-                                Integer femaleAssigned = ((Number) classItem.get("female_assigned")).intValue();
-
-                                XgStudentTreeVo classNode = new XgStudentTreeVo();
-                                classNode.setId("class-" + classId);
-                                classNode.setLabel(className);
-                                classNode.setType("class");
-                                classNode.setCollegeId(collegeId);
-                                classNode.setCollegeName(collegeName);
-                                classNode.setMajorId(majorId);
-                                classNode.setMajorName(majorName);
-                                classNode.setClassId(classId);
-                                classNode.setClassName(className);
-                                classNode.setGrade(grade);
-                                classNode.setMaleTotal(maleTotal);
-                                classNode.setFemaleTotal(femaleTotal);
-                                classNode.setMaleAssigned(maleAssigned);
-                                classNode.setFemaleAssigned(femaleAssigned);
-
-                                majorNode.getChildren().add(classNode);
-
-                                // 累计专业人数
-                                majorMaleTotal += maleTotal;
-                                majorFemaleTotal += femaleTotal;
-                                majorMaleAssigned += maleAssigned;
-                                majorFemaleAssigned += femaleAssigned;
-                            }
-
-                            // 设置专业统计
-                            majorNode.setMaleTotal(majorMaleTotal);
-                            majorNode.setFemaleTotal(majorFemaleTotal);
-                            majorNode.setMaleAssigned(majorMaleAssigned);
-                            majorNode.setFemaleAssigned(majorFemaleAssigned);
-
-                            collegeNode.getChildren().add(majorNode);
-
-                            // 累计学院人数
-                            collegeMaleTotal += majorMaleTotal;
-                            collegeFemaleTotal += majorFemaleTotal;
-                            collegeMaleAssigned += majorMaleAssigned;
-                            collegeFemaleAssigned += majorFemaleAssigned;
-                        }
-                    }
+                XgStudentTreeVo collegeNode = new XgStudentTreeVo();
+                collegeNode.setId("college-" + collegeId);
+                collegeNode.setLabel(collegeName);
+                collegeNode.setType("college");
+                collegeNode.setCollegeId(collegeId);
+                collegeNode.setCollegeName(collegeName);
+                collegeNode.setMaleTotal(maleTotal);
+                collegeNode.setFemaleTotal(femaleTotal);
+                collegeNode.setMaleAssigned(maleAssigned);
+                collegeNode.setFemaleAssigned(femaleAssigned);
+                // 设置为null表示不展示子节点
+                collegeNode.setChildren(null);
 
-                    // 设置学院统计
-                    collegeNode.setMaleTotal(collegeMaleTotal);
-                    collegeNode.setFemaleTotal(collegeFemaleTotal);
-                    collegeNode.setMaleAssigned(collegeMaleAssigned);
-                    collegeNode.setFemaleAssigned(collegeFemaleAssigned);
+                result.add(collegeNode);
 
-                    result.add(collegeNode);
-                }
+                log.debug("学院 {} 统计 - 男生总数: {}, 女生总数: {}, 男生已分配: {}, 女生已分配: {}",
+                        collegeName, maleTotal, femaleTotal, maleAssigned, femaleAssigned);
             }
 
             log.info("学生信息树构建完成,共 {} 个学院节点", result.size());

+ 46 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml

@@ -250,4 +250,50 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name
     </select>
 
+    <!-- 查询二级学院下的学院床位分配统计信息(基于学生表统计需求,基于床位分配表统计已分配) -->
+    <select id="getCollegeBedAssignmentStats" resultType="java.util.Map">
+        SELECT
+            sm.college_id,
+            sm.college_name,
+            COALESCE(SUM(CASE WHEN sm.gender = '0' AND sm.is_boarder = 1 THEN 1 ELSE 0 END), 0) AS male_total,
+            COALESCE(SUM(CASE WHEN sm.gender = '1' AND sm.is_boarder = 1 THEN 1 ELSE 0 END), 0) AS female_total,
+            COALESCE((
+                SELECT COUNT(*)
+                FROM xg_dorm_bed_student dbs
+                LEFT JOIN xg_dorm_bed db ON dbs.bed_id = db.id
+                LEFT JOIN xg_dorm_room dr ON db.room_id = dr.id
+                LEFT JOIN xg_dorm_building dbuild ON dr.building_id = dbuild.id
+                WHERE dbs.college_id = sm.college_id
+                  AND dbs.deleted = '0'
+                  AND dbs.status = '0'
+                  AND db.deleted = '0'
+                  AND dr.deleted = '0'
+                  AND dbuild.deleted = '0'
+                  AND dbuild.building_type = '1'
+            ), 0) AS male_assigned,
+            COALESCE((
+                SELECT COUNT(*)
+                FROM xg_dorm_bed_student dbs
+                LEFT JOIN xg_dorm_bed db ON dbs.bed_id = db.id
+                LEFT JOIN xg_dorm_room dr ON db.room_id = dr.id
+                LEFT JOIN xg_dorm_building dbuild ON dr.building_id = dbuild.id
+                WHERE dbs.college_id = sm.college_id
+                  AND dbs.deleted = '0'
+                  AND dbs.status = '0'
+                  AND db.deleted = '0'
+                  AND dr.deleted = '0'
+                  AND dbuild.deleted = '0'
+                  AND dbuild.building_type = '2'
+            ), 0) AS female_assigned
+        FROM xg_student_msg sm
+        LEFT JOIN sys_dept d ON sm.college_id = d.dept_id
+        WHERE sm.deleted = 0
+          AND sm.student_status = '1'
+          AND sm.is_boarder = 1
+          AND (d.parent_id = #{secondaryCollegeId} OR d.dept_id = #{secondaryCollegeId} OR FIND_IN_SET(#{secondaryCollegeId}, d.ancestors))
+          AND sm.college_id IS NOT NULL
+        GROUP BY sm.college_id, sm.college_name
+        ORDER BY sm.college_name
+    </select>
+
 </mapper>

+ 1 - 0
docs/开发文档/API/API文档总览.md

@@ -82,6 +82,7 @@
 | 处理预约申请 | POST | /api/xljk/consultation/appointment/process | 预约管理 | xljk:consultation:appointment:process |
 | **获取房间床位详细信息** | GET | /dormbed/dormBed/details/room/{roomId} | 宿舍管理 | dormbed:dormBed:query |
 | **获取宿舍楼床位详细信息** | GET | /dormbed/dormBed/details/building/{buildingId} | 宿舍管理 | dormbed:dormBed:query |
+| 获取当前班主任的班级列表 | GET | /classMsg/classMsg/myClassListAsTeacher | 班级信息 | classMsg:classMsg:list |
 | 记录咨询结果 | POST | /api/xljk/consultation/appointment/record | 预约管理 | xljk:consultation:appointment:record |
 | 查询未到访记录列表 | GET | /api/xljk/consultation/no-show/list | 预约限制 | xljk:consultation:noshow:list |
 | 获取未到访记录详情 | GET | /api/xljk/consultation/no-show/{id} | 预约限制 | xljk:consultation:noshow:query |

+ 25 - 0
docs/开发文档/API/班级信息管理API.md

@@ -98,6 +98,31 @@
 }
 ```
 
+### 1.3 获取当前登录人作为班主任的班级列表
+
+- 接口URL: `/classMsg/classMsg/myClassListAsTeacher`
+- 请求方式: GET
+- 权限标识: `classMsg:classMsg:list`
+- 请求参数: 无
+- 返回参数:
+
+```json
+{
+  "code": 200,
+  "msg": "操作成功",
+  "data": [
+    {
+      "id": 1,
+      "className": "计科2025-01班",
+      "majorId": 2001,
+      "majorName": "计算机科学与技术",
+      "grade": "2025",
+      "classStatus": 1
+    }
+  ]
+}
+```
+
 ### 2. 班级信息维护接口
 
 #### 2.1 新增班级信息

+ 93 - 0
web/src/api/xg/xgss/dormBedStudent/dormBedStudent/index.ts

@@ -94,3 +94,96 @@ export const getCurrentUserCollegeDormTree = (): AxiosPromise<DormBuildingTreeVO
     method: 'get'
   });
 };
+
+// ============== 班主任分配相关API ==============
+
+/**
+ * 获取班主任管理的班级宿舍楼树形结构
+ * 只显示班主任管理班级分配到的宿舍楼
+ */
+export const getClassTeacherDormTree = (classId?: string | number): AxiosPromise<DormBuildingTreeVO[]> => {
+  return request({
+    url: '/dormBedStudent/dormBedStudent/getClassTeacherDormTree',
+    method: 'get',
+    params: { classId }
+  });
+};
+
+/**
+ * 获取班主任管理班级的房间床位详情(带学生信息)
+ * @param buildingId 宿舍楼ID,为空则查询所有楼栋
+ */
+export const getClassTeacherBedDetails = (
+  buildingId?: string | number,
+  classId?: string | number
+): AxiosPromise<any[]> => {
+  return request({
+    url: '/dormBedStudent/dormBedStudent/getClassTeacherBedDetails',
+    method: 'get',
+    params: { buildingId, classId }
+  });
+};
+
+/**
+ * 获取班主任管理的未分配床位学生列表
+ */
+export const getClassTeacherUnallocatedStudents = (classId?: string | number): AxiosPromise<any[]> => {
+  return request({
+    url: '/dormBedStudent/dormBedStudent/getClassTeacherUnallocatedStudents',
+    method: 'get',
+    params: { classId }
+  });
+};
+
+/**
+ * 班主任一键随机分配床位
+ * @param buildingId 宿舍楼ID,为空则全部分配
+ */
+export const classTeacherRandomAllocation = (buildingId?: string | number, classId?: string | number) => {
+  return request({
+    url: '/dormBedStudent/dormBedStudent/classTeacherRandomAllocation',
+    method: 'post',
+    params: { buildingId, classId }
+  });
+};
+
+/**
+ * 班主任一键按学号分配床位
+ * @param buildingId 宿舍楼ID,为空则全部分配
+ */
+export const classTeacherStudentNoAllocation = (buildingId?: string | number, classId?: string | number) => {
+  return request({
+    url: '/dormBedStudent/dormBedStudent/classTeacherStudentNoAllocation',
+    method: 'post',
+    params: { buildingId, classId }
+  });
+};
+
+/**
+ * 班主任选择性分配床位(弹窗模式)
+ * @param allocationParams 分配参数 {bedIds: number[], studentIds: number[], allocationType: 'random' | 'studentNo'}
+ */
+export const classTeacherSelectiveAllocation = (allocationParams: {
+  bedIds: Array<string | number>;
+  studentIds: Array<string | number>;
+  allocationType: 'random' | 'studentNo';
+}, classId?: string | number) => {
+  return request({
+    url: '/dormBedStudent/dormBedStudent/classTeacherSelectiveAllocation',
+    method: 'post',
+    data: allocationParams,
+    params: { classId }
+  });
+};
+
+/**
+ * 班主任一键收回床位
+ * @param buildingId 宿舍楼ID,为空则全部收回
+ */
+export const classTeacherRecallBeds = (buildingId?: string | number, classId?: string | number) => {
+  return request({
+    url: '/dormBedStudent/dormBedStudent/classTeacherRecallBeds',
+    method: 'post',
+    params: { buildingId, classId }
+  });
+};

+ 8 - 0
web/src/api/xg/xsjcxx/classMsg/classMsg/index.ts

@@ -44,3 +44,11 @@ export function delClassMsg(id: string | number) {
     method: 'delete'
   });
 }
+
+// 获取当前登录人作为班主任的班级列表
+export function getMyClassListAsTeacher(): AxiosPromise<ClassMsgVO[]> {
+  return request({
+    url: '/classMsg/classMsg/myClassListAsTeacher',
+    method: 'get'
+  });
+}

+ 990 - 0
web/src/views/xg/xgss/bzrfp/bzrfp.vue

@@ -0,0 +1,990 @@
+<template>
+  <div class="p-2">
+    <!-- 班级 Tabs -->
+    <el-card shadow="hover" class="mb-4 class-card">
+      <template #header>
+        <div class="flex items-center justify-between">
+          <div class="flex items-center gap-3">
+            <span class="font-bold">班主任班级</span>
+            <el-tabs v-if="classList.length > 0" v-model="activeClassId" type="card" class="class-tabs">
+              <el-tab-pane v-for="cls in classList" :key="String(cls.id)" :label="cls.className" :name="String(cls.id)" />
+            </el-tabs>
+          </div>
+          <span v-if="classList.length === 0" class="text-gray-500 text-sm">当前账号没有班主任班级</span>
+        </div>
+      </template>
+    </el-card>
+
+    <div class="flex-layout">
+      <!-- 宿舍楼树 -->
+      <div class="tree-sidebar">
+        <el-card shadow="hover" class="tree-card">
+          <div class="card-header-title">宿舍楼</div>
+          <el-input
+            v-model="buildingName"
+            prefix-icon="Search"
+            placeholder="请输入宿舍楼名称"
+            clearable
+            @clear="watchBuildingName"
+            @keyup.enter="watchBuildingName"
+          >
+          </el-input>
+          <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="!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" v-if="!activeClassId">请先选择上方班级</p>
+            <p class="text-gray-500 text-lg" v-else>请在左侧选择宿舍楼</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="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="warning" icon="RefreshLeft" size="small" @click="handleRecallBeds">收回床位</el-button>
+                </el-col>
+                <el-col :span="12" style="text-align: right">
+                  <el-button
+                    link
+                    type="primary"
+                    size="small"
+                    :loading="expandAllLoading"
+                    :disabled="expandAllLoading || selectionLoading || loading"
+                    @click="expandAll"
+                    >全部展开</el-button
+                  >
+                  <el-button
+                    link
+                    type="primary"
+                    size="small"
+                    :loading="collapseAllLoading"
+                    :disabled="collapseAllLoading || selectionLoading || 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"> 已选择: {{ selectedRoomCount }}个房间 / {{ 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>
+                      <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>
+              </el-row>
+            </div>
+
+            <!-- 房间及床位树形表格 -->
+            <el-table
+              ref="tableRef"
+              v-loading="loading || selectionLoading || expandAllLoading || collapseAllLoading"
+              element-loading-text="处理中..."
+              :data="roomList"
+              row-key="id"
+              border
+              :expand-row-keys="expandedRowKeys"
+              @expand-change="handleExpandChange"
+              :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+              table-layout="auto"
+              @selection-change="handleSelectionChange"
+              @select-all="handleSelectAll"
+              :row-class-name="getRowClassName"
+            >
+              <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.collegeName">{{ scope.row.collegeName }}</span>
+                  <span v-else>-</span>
+                </template>
+              </el-table-column>
+              <!-- 专业信息列 -->
+              <el-table-column label="专业" width="120" align="center">
+                <template #default="scope">
+                  <span v-if="scope.row.isBed && scope.row.majorName">{{ scope.row.majorName }}</span>
+                  <span v-else>-</span>
+                </template>
+              </el-table-column>
+              <!-- 班级信息列 -->
+              <el-table-column label="班级" width="120" align="center">
+                <template #default="scope">
+                  <span v-if="scope.row.isBed && scope.row.className">{{ scope.row.className }}</span>
+                  <span v-else>-</span>
+                </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="selectiveAllocationDialog.visible"
+      :title="selectiveAllocationDialog.title"
+      :building-info="selectiveAllocationDialog.buildingInfo"
+      :class-id="activeClassId"
+      @success="handleAllocationSuccess"
+    />
+  </div>
+</template>
+
+<script setup name="ClassTeacherBedAllocation" lang="ts">
+import { ref, computed, reactive, onMounted, nextTick, watch } from 'vue';
+import { InfoFilled } 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';
+import { getMyClassListAsTeacher } from '@/api/xg/xsjcxx/classMsg/classMsg';
+// 定义一个最小班级信息类型以避免跨文件类型不兼容
+interface SimpleClassVO {
+  id: string | number;
+  className: string;
+  collegeId?: string | number;
+  majorId?: string | number;
+}
+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 expandedRowKeys = ref<string[]>([]);
+
+// 加载状态:展开/收起/选择
+const expandAllLoading = ref(false);
+const collapseAllLoading = ref(false);
+const selectionLoading = ref(false);
+
+// 小延迟,确保加载动画先渲染
+const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
+
+// 班级 Tab 数据
+const classList = ref<SimpleClassVO[]>([]);
+const activeClassId = ref<string>('');
+
+// 选择状态
+const selectedRows = ref<any[]>([]);
+const selectedRoomCount = ref(0);
+const selectedBedCount = ref(0);
+// 床位状态统计
+const selectedBedStats = ref({
+  available: 0, // 可分配床位(空闲)
+  allocated: 0, // 已分配床位
+  maintenance: 0 // 维修中床位
+});
+
+// 未分配学生数量
+const unallocatedStudentCount = ref(0);
+
+const animate = reactive({
+  searchAnimate: {
+    enter: 'animate__animated animate__fadeInDown',
+    leave: 'animate__animated animate__fadeOutUp'
+  }
+});
+
+// 宿舍树数据
+const dormOptions = ref<DormBuildingTreeVO[]>([]);
+const selectedBuilding = ref<DormBuildingTreeVO>();
+
+// 房间和床位数据(合并展示)
+const roomList = ref<any[]>([]);
+
+// 添加请求取消控制器
+let currentController: AbortController | null = null;
+
+// 床位到房间的映射缓存,优化性能
+const bedToRoomMap = ref<Map<string, any>>(new Map());
+
+// 查询参数
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10000,
+  buildingId: undefined as string | number | undefined,
+  roomName: '',
+  roomStatus: undefined as string | undefined
+});
+
+// 选择性分配对话框
+const selectiveAllocationDialog = reactive({
+  visible: false,
+  title: '选择性分配床位',
+  buildingInfo: undefined as DormBuildingTreeVO | undefined
+});
+
+/** 通过条件过滤节点 */
+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);
+};
+
+// 监听buildingName变化,实时筛选宿舍楼
+watch(buildingName, () => {
+  watchBuildingName();
+});
+
+// 监听班级切换
+watch(activeClassId, async () => {
+  await initForActiveClass();
+});
+
+/** 加载当前班主任班级列表 */
+const loadClassList = async () => {
+  try {
+    const res = await getMyClassListAsTeacher();
+    classList.value = (res.data || []) as any;
+    if (classList.value.length > 0) {
+      activeClassId.value = String(classList.value[0].id);
+      await initForActiveClass();
+    }
+  } catch (e) {
+    console.error('获取班主任班级列表失败', e);
+  }
+};
+
+/** 切换班级Tab */
+const handleClassTabChange = async () => {
+  await initForActiveClass();
+};
+
+/** 初始化选中班级的界面数据 */
+const initForActiveClass = async () => {
+  // 重置筛选与选择
+  buildingName.value = '';
+  selectedBuilding.value = undefined;
+  queryParams.buildingId = undefined;
+  roomList.value = [];
+  expandedRowKeys.value = [];
+  selectedRows.value = [];
+  selectedRoomCount.value = 0;
+  selectedBedCount.value = 0;
+  selectedBedStats.value = { available: 0, allocated: 0, maintenance: 0 };
+  unallocatedStudentCount.value = 0;
+  // 重新加载宿舍树
+  await getDormTreeData();
+};
+
+/** 获取班主任宿舍树数据 */
+const getDormTreeData = async () => {
+  try {
+    console.log('获取班主任管理的班级宿舍楼树');
+    const res = await getClassTeacherDormTree(activeClassId.value);
+    dormOptions.value = res.data || [];
+  } catch (error) {
+    console.error('获取班主任管理的班级宿舍楼树失败', error);
+  }
+};
+
+/** 处理节点单击事件 */
+const handleNodeClick = (data: DormBuildingTreeVO) => {
+  if (data.type !== 'building') {
+    return;
+  }
+
+  // 取消之前未完成的请求
+  if (currentController) {
+    currentController.abort();
+    currentController = null;
+  }
+
+  // 设置选中的宿舍楼
+  selectedBuilding.value = data;
+  // 更新查询参数
+  queryParams.buildingId = data.id;
+  // 加载房间数据
+  handleQuery();
+};
+
+/** 查询房间数据 */
+const handleQuery = () => {
+  // 创建新的AbortController
+  currentController = new AbortController();
+  loadRoomData();
+};
+
+/** 重置查询条件 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  if (selectedBuilding.value) {
+    queryParams.buildingId = selectedBuilding.value.id;
+  }
+  handleQuery();
+};
+
+/** 加载房间数据 */
+const loadRoomData = async () => {
+  if (!queryParams.buildingId) return;
+
+  loading.value = true;
+  const controller = currentController;
+
+  try {
+    // 获取班主任管理班级的房间床位详情
+    const res = await getClassTeacherBedDetails(queryParams.buildingId, activeClassId.value);
+
+    if (controller?.signal?.aborted) return;
+
+    roomList.value = res.data || [];
+
+    // 构建床位到房间的映射缓存
+    buildBedToRoomMap();
+
+    // 清除可选行缓存,强制重新计算
+    selectableRowsCache.value = [];
+
+    // 重置选择状态
+    isUpdatingSelection.value = true;
+    try {
+      selectedRows.value = [];
+      selectedRoomCount.value = 0;
+      selectedBedCount.value = 0;
+      selectedBedStats.value = { available: 0, allocated: 0, maintenance: 0 };
+
+      // 确保表格选择状态与数据一致
+      nextTick(() => {
+        tableRef.value?.clearSelection();
+      });
+    } finally {
+      isUpdatingSelection.value = false;
+    }
+
+    // 获取未分配学生数量
+    await getUnallocatedStudentCount();
+  } catch (error: any) {
+    if (error.name !== 'AbortError') {
+      console.error('加载房间数据失败', error);
+    }
+  } finally {
+    if (controller === currentController) {
+      loading.value = false;
+    }
+  }
+};
+
+/** 获取未分配学生数量 */
+const getUnallocatedStudentCount = async () => {
+  try {
+    const res = await getClassTeacherUnallocatedStudents(activeClassId.value);
+    unallocatedStudentCount.value = res.data?.length || 0;
+  } catch (error) {
+    console.error('获取未分配学生数量失败', error);
+  }
+};
+
+/** 选择性分配 */
+const handleSelectiveAllocation = () => {
+  if (!selectedBuilding.value) {
+    ElMessage.warning('请先选择宿舍楼');
+    return;
+  }
+
+  selectiveAllocationDialog.buildingInfo = selectedBuilding.value;
+  selectiveAllocationDialog.visible = true;
+};
+
+/** 随机分配 */
+const handleRandomAllocation = async () => {
+  if (!selectedBuilding.value) {
+    ElMessage.warning('请先选择宿舍楼');
+    return;
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确认对 ${selectedBuilding.value.label} 进行随机分配吗?\n\n系统将自动为未分配床位的学生随机分配可用床位。`,
+      '确认随机分配',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    );
+
+    await classTeacherRandomAllocation(selectedBuilding.value.id, activeClassId.value);
+    ElMessage.success('随机分配成功');
+    handleAllocationSuccess();
+  } catch (error: any) {
+    if (error !== 'cancel') {
+      ElMessage.error('随机分配失败');
+    }
+  }
+};
+
+/** 按学号分配 */
+const handleStudentNoAllocation = async () => {
+  if (!selectedBuilding.value) {
+    ElMessage.warning('请先选择宿舍楼');
+    return;
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确认对 ${selectedBuilding.value.label} 按学号进行分配吗?\n\n系统将按照学号顺序为未分配床位的学生分配可用床位。`,
+      '确认按学号分配',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    );
+
+    await classTeacherStudentNoAllocation(selectedBuilding.value.id, activeClassId.value);
+    ElMessage.success('按学号分配成功');
+    handleAllocationSuccess();
+  } catch (error: any) {
+    if (error !== 'cancel') {
+      ElMessage.error('按学号分配失败');
+    }
+  }
+};
+
+/** 收回床位 */
+const handleRecallBeds = async () => {
+  if (!selectedBuilding.value) {
+    ElMessage.warning('请先选择宿舍楼');
+    return;
+  }
+
+  try {
+    await ElMessageBox.confirm(`确认收回 ${selectedBuilding.value.label} 的所有床位分配吗?\n\n收回后,学生的床位信息将被清空。`, '确认收回床位', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+
+    await classTeacherRecallBeds(selectedBuilding.value.id, activeClassId.value);
+    ElMessage.success('收回床位成功');
+    handleAllocationSuccess();
+  } catch (error: any) {
+    if (error !== 'cancel') {
+      ElMessage.error('收回床位失败');
+    }
+  }
+};
+
+/** 分配成功回调 */
+const handleAllocationSuccess = () => {
+  // 清除缓存,确保数据刷新
+  selectableRowsCache.value = [];
+  // 刷新房间和床位数据
+  loadRoomData();
+  // 清空选择状态
+  isUpdatingSelection.value = true;
+  try {
+    selectedRows.value = [];
+    selectedRoomCount.value = 0;
+    selectedBedCount.value = 0;
+    selectedBedStats.value = { available: 0, allocated: 0, maintenance: 0 };
+  } finally {
+    isUpdatingSelection.value = false;
+  }
+};
+
+/** 全部展开 */
+const expandAll = async () => {
+  if (expandAllLoading.value || selectionLoading.value || !roomList.value?.length) return;
+  expandAllLoading.value = true;
+  await nextTick();
+  await delay(16);
+  expandedRowKeys.value = roomList.value.map((room) => String(room.id));
+  await nextTick();
+  expandAllLoading.value = false;
+};
+
+/** 全部收起 */
+const collapseAll = async () => {
+  if (collapseAllLoading.value || selectionLoading.value) return;
+  collapseAllLoading.value = true;
+  await nextTick();
+  await delay(16);
+  expandedRowKeys.value = [];
+  await nextTick();
+  collapseAllLoading.value = false;
+};
+
+/** 处理行的展开/收起 */
+const handleExpandChange = (row: any, expanded: boolean) => {
+  const expandKey = String(row.id);
+  if (expanded) {
+    expandedRowKeys.value.push(expandKey);
+  } else {
+    const index = expandedRowKeys.value.findIndex((key) => key === expandKey);
+    if (index !== -1) {
+      expandedRowKeys.value.splice(index, 1);
+    }
+  }
+};
+
+/** 检查行是否可选中 */
+const checkSelectable = (row: any) => {
+  if (row.isBed) {
+    const parentRoom = findParentRoom(row);
+    return parentRoom && parentRoom.roomStatus !== '1' && parentRoom.roomStatus !== '2';
+  } else {
+    return row.roomStatus !== '1' && row.roomStatus !== '2';
+  }
+};
+
+/** 构建床位到房间的映射缓存 */
+const buildBedToRoomMap = () => {
+  const map = new Map();
+  roomList.value.forEach((room) => {
+    if (room.children && room.children.length > 0) {
+      room.children.forEach((bed: any) => {
+        map.set(bed.id, room);
+      });
+    }
+  });
+  bedToRoomMap.value = map;
+};
+
+/** 查找床位的父房间 */
+const findParentRoom = (bed: any) => {
+  return bedToRoomMap.value.get(bed.id);
+};
+
+/** 获取行类名,用于区分房间和床位 */
+const getRowClassName = ({ row }: { row: any }) => {
+  if (!row.isBed) {
+    return 'room-row';
+  }
+  return 'bed-row';
+};
+
+/** 获取所有可选择的行 */
+const getAllSelectableRows = () => {
+  if (selectableRowsCache.value.length > 0) {
+    return selectableRowsCache.value;
+  }
+
+  const selectableRows: any[] = [];
+
+  roomList.value.forEach((room) => {
+    const isRoomSelectable = room.roomStatus !== '1' && room.roomStatus !== '2';
+
+    if (isRoomSelectable) {
+      selectableRows.push(room);
+    }
+
+    if (room.children && room.children.length > 0) {
+      room.children.forEach((bed: any) => {
+        if (isRoomSelectable) {
+          selectableRows.push(bed);
+        }
+      });
+    }
+  });
+
+  selectableRowsCache.value = selectableRows;
+  return selectableRows;
+};
+
+// 添加可选行缓存
+const selectableRowsCache = ref<any[]>([]);
+
+/** 处理全选事件 */
+const handleSelectAll = async (selection: any[]) => {
+  if (selectionLoading.value) return;
+
+  selectionLoading.value = true;
+  isUpdatingSelection.value = true;
+
+  try {
+    const isSelectAll = selection.length > 0;
+
+    await nextTick();
+    await delay(16);
+
+    if (isSelectAll) {
+      const availableRows = getAllSelectableRows();
+      tableRef.value?.clearSelection();
+      availableRows.forEach((row) => {
+        tableRef.value?.toggleRowSelection(row, true);
+      });
+      selectedRows.value = [...availableRows];
+      updateSelectionCount(availableRows);
+    } else {
+      tableRef.value?.clearSelection();
+      selectedRows.value = [];
+      selectedRoomCount.value = 0;
+      selectedBedCount.value = 0;
+      selectedBedStats.value = { available: 0, allocated: 0, maintenance: 0 };
+    }
+  } finally {
+    isUpdatingSelection.value = false;
+    selectionLoading.value = false;
+  }
+};
+
+/** 更新选择统计 */
+const updateSelectionCount = (rows: any[]) => {
+  const uniqueRooms = new Set();
+  const uniqueBeds = new Set();
+
+  const bedStats = {
+    available: 0,
+    allocated: 0,
+    maintenance: 0
+  };
+
+  rows.forEach((row) => {
+    if (row.isBed) {
+      uniqueBeds.add(row.id);
+      if (row.bedStatus === '2') {
+        bedStats.maintenance++;
+      } else if (row.studentId) {
+        bedStats.allocated++;
+      } else {
+        bedStats.available++;
+      }
+    } else {
+      uniqueRooms.add(row.id);
+    }
+  });
+
+  selectedRoomCount.value = uniqueRooms.size;
+  selectedBedCount.value = uniqueBeds.size;
+  selectedBedStats.value = bedStats;
+};
+
+// 添加标志位避免循环触发
+const isUpdatingSelection = ref(false);
+
+/** 处理选择变更 */
+const handleSelectionChange = (rows: any[]) => {
+  if (isUpdatingSelection.value) return;
+
+  handleRoomBedSelection(rows);
+  selectedRows.value = rows;
+  updateSelectionCount(rows);
+};
+
+/** 处理房间和床位的联动选择逻辑 */
+const handleRoomBedSelection = (currentSelection: any[]) => {
+  const previousSelection = selectedRows.value;
+
+  const addedItems = currentSelection.filter((current) => !previousSelection.some((prev) => prev.id === current.id && prev.isBed === current.isBed));
+  const removedItems = previousSelection.filter(
+    (prev) => !currentSelection.some((current) => current.id === prev.id && current.isBed === prev.isBed)
+  );
+
+  if (addedItems.length === 0 && removedItems.length === 0) {
+    return;
+  }
+
+  isUpdatingSelection.value = true;
+
+  try {
+    const addedRooms = addedItems.filter((item) => !item.isBed);
+    addedRooms.forEach((room) => {
+      selectRoomBeds(room, true);
+    });
+
+    const removedRooms = removedItems.filter((item) => !item.isBed);
+    removedRooms.forEach((room) => {
+      selectRoomBeds(room, false);
+    });
+  } finally {
+    isUpdatingSelection.value = false;
+  }
+};
+
+/** 选择或取消选择房间下的所有可用床位 */
+const selectRoomBeds = (room: any, isSelected: boolean) => {
+  if (!room?.children?.length) return;
+
+  const availableBeds = room.children.filter((bed: any) => room.roomStatus !== '1' && room.roomStatus !== '2');
+
+  if (availableBeds.length === 0) return;
+
+  availableBeds.forEach((bed: any) => {
+    tableRef.value?.toggleRowSelection(bed, isSelected);
+  });
+};
+
+// 组件挂载时获取数据
+onMounted(async () => {
+  await loadClassList();
+  if (currentController) {
+    currentController.abort();
+    currentController = null;
+  }
+});
+</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(100vh - 200px);
+  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;
+}
+
+.empty-tip {
+  height: 100%;
+}
+
+.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;
+}
+
+.el-table :deep(.room-row) {
+  background-color: #eefeff !important;
+}
+
+.el-table :deep(.bed-row) {
+  background-color: #ffffff !important;
+}
+
+.el-table :deep(.room-row:hover) {
+  background-color: #d1f0ff !important;
+}
+
+.el-table :deep(.bed-row:hover) {
+  background-color: #e8f0f5 !important;
+}
+.class-card :deep(.el-card__header) {
+  padding: 12px 12px 0 12px !important;
+  margin: 0 !important;
+}
+.class-card :deep(.el-card__body) {
+  padding: 0 !important;
+}
+.class-tabs {
+  margin-left: 12px;
+}
+.class-tabs :deep(.el-tabs__header) {
+  margin: 0;
+}
+.class-tabs :deep(.el-tabs__nav-wrap) {
+  margin-bottom: 0;
+}
+</style>

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

@@ -0,0 +1,537 @@
+<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';
+
+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, 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 = () => {
+  // 实时搜索,computed 已处理
+};
+
+/** 学生搜索 */
+const handleStudentSearch = () => {
+  // 实时搜索,computed 已处理
+};
+
+/** 检查床位是否可选 */
+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>

+ 1 - 1
web/src/views/xg/xgss/xsssfpejxy/index.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"