This commit is contained in:
zlwu committed 2025-08-14 23:34:15 +08:00
1 parent 6aa2345f2e
commit 4075b903cf
6 files changed
+1216

No files matched your search

@@ -270,4 +270,55 @@ public class XgDormBedStudentController extends BaseController {
return toAjax(xgDormBedStudentService.classTeacherStayawaySelectedBeds(bedIds, classId));
}
/**
* 班主任床位互换
* 将两个有学生的床位进行互换
*/
@SaCheckPermission("dormBedStudent:dormBedStudent:edit")
@Log(title = "班主任床位互换", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/classTeacherExchangeBeds")
public R<Void> classTeacherExchangeBeds(@RequestParam Long sourceBedId,
@RequestParam Long targetBedId,
@RequestParam(required = false) Long classId) {
return toAjax(xgDormBedStudentService.classTeacherExchangeBeds(sourceBedId, targetBedId, classId));
}
/**
* 班主任床位移动
* 将学生从当前床位移动到指定的空闲床位
*/
@SaCheckPermission("dormBedStudent:dormBedStudent:edit")
@Log(title = "班主任床位移动", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/classTeacherMoveBed")
public R<Void> classTeacherMoveBed(@RequestParam Long sourceBedId,
@RequestParam Long targetBedId,
@RequestParam(required = false) Long classId) {
return toAjax(xgDormBedStudentService.classTeacherMoveBed(sourceBedId, targetBedId, classId));
}
/**
* 获取班级所有已入住宿舍的学生床位信息
* 包含床位信息(校区名称、宿舍区名称、楼栋名称、房间名称、床位名称)
* 和学生信息(学生姓名、学号、性别)
*/
@SaCheckPermission("dormBedStudent:dormBedStudent:list")
@GetMapping("/getClassOccupiedBedDetails")
public R<List<Object>> getClassOccupiedBedDetails(@RequestParam(required = false) Long classId) {
List<Object> occupiedBedDetails = xgDormBedStudentService.getClassOccupiedBedDetails(classId);
return R.ok(occupiedBedDetails);
}
/**
* 获取班级所有未安排学生的床位信息
* 包含床位信息(校区名称、宿舍区名称、楼栋名称、房间名称、床位名称)
*/
@SaCheckPermission("dormBedStudent:dormBedStudent:list")
@GetMapping("/getClassUnoccupiedBedDetails")
public R<List<Object>> getClassUnoccupiedBedDetails(@RequestParam(required = false) Long classId) {
List<Object> unoccupiedBedDetails = xgDormBedStudentService.getClassUnoccupiedBedDetails(classId);
return R.ok(unoccupiedBedDetails);
}
}
@@ -192,4 +192,45 @@ public interface IXgDormBedStudentService {
* @return 是否退宿成功
*/
Boolean classTeacherStayawaySelectedBeds(List<Long> bedIds, Long classId);
/**
* 班主任床位互换
* 将两个有学生的床位进行互换
*
* @param sourceBedId 源床位ID
* @param targetBedId 目标床位ID
* @param classId 班级ID
* @return 是否互换成功
*/
Boolean classTeacherExchangeBeds(Long sourceBedId, Long targetBedId, Long classId);
/**
* 班主任床位移动
* 将学生从当前床位移动到指定的空闲床位
*
* @param sourceBedId 源床位ID
* @param targetBedId 目标床位ID
* @param classId 班级ID
* @return 是否移动成功
*/
Boolean classTeacherMoveBed(Long sourceBedId, Long targetBedId, Long classId);
/**
* 获取班级所有已入住宿舍的学生床位信息
* 包含床位信息(校区名称、宿舍区名称、楼栋名称、房间名称、床位名称)
* 和学生信息(学生姓名、学号、性别)
*
* @param classId 班级ID,可为空
* @return 已入住床位详情列表
*/
List<Object> getClassOccupiedBedDetails(Long classId);
/**
* 获取班级所有未安排学生的床位信息
* 包含床位信息(校区名称、宿舍区名称、楼栋名称、房间名称、床位名称)
*
* @param classId 班级ID,可为空
* @return 未安排学生的床位详情列表
*/
List<Object> getClassUnoccupiedBedDetails(Long classId);
}
@@ -1873,4 +1873,323 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
}
}
/**
* 班主任床位互换
* 将两个有学生的床位进行互换
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean classTeacherExchangeBeds(Long sourceBedId, Long targetBedId, Long classId) {
try {
log.info("班主任床位互换开始,源床位ID: {}, 目标床位ID: {}, 班级ID: {}", sourceBedId, targetBedId, classId);
// 1. 校验参数
if (sourceBedId == null || targetBedId == null) {
throw new RuntimeException("床位ID不能为空");
}
if (sourceBedId.equals(targetBedId)) {
throw new RuntimeException("不能选择相同的床位进行互换");
}
// 2. 获取源床位和目标床位的分配信息
LambdaQueryWrapper<XgDormBedStudent> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.in(XgDormBedStudent::getBedId, sourceBedId, targetBedId)
.eq(XgDormBedStudent::getDeleted, "0");
List<XgDormBedStudent> bedStudents = baseMapper.selectList(queryWrapper);
if (bedStudents.size() != 2) {
throw new RuntimeException("互换床位必须都已分配学生");
}
// 3. 找到对应的分配记录
XgDormBedStudent sourceBedStudent = bedStudents.stream()
.filter(bs -> bs.getBedId().equals(sourceBedId))
.findFirst()
.orElseThrow(() -> new RuntimeException("未找到源床位的分配记录"));
XgDormBedStudent targetBedStudent = bedStudents.stream()
.filter(bs -> bs.getBedId().equals(targetBedId))
.findFirst()
.orElseThrow(() -> new RuntimeException("未找到目标床位的分配记录"));
// 4. 验证班级权限(如果指定了班级ID)
if (classId != null) {
if (!classId.equals(sourceBedStudent.getClassId()) && !classId.equals(targetBedStudent.getClassId())) {
throw new RuntimeException("没有权限操作这些床位");
}
}
// 5. 执行互换:交换两个记录的床位ID
Long tempStudentId = sourceBedStudent.getStudentId();
Long tempClassId = sourceBedStudent.getClassId();
// 更新源床位分配记录
sourceBedStudent.setStudentId(targetBedStudent.getStudentId());
sourceBedStudent.setClassId(targetBedStudent.getClassId());
sourceBedStudent.setUpdateTime(new Date());
sourceBedStudent.setUpdateBy(LoginHelper.getUserId());
// 更新目标床位分配记录
targetBedStudent.setStudentId(tempStudentId);
targetBedStudent.setClassId(tempClassId);
targetBedStudent.setUpdateTime(new Date());
targetBedStudent.setUpdateBy(LoginHelper.getUserId());
// 6. 更新数据库
baseMapper.updateById(sourceBedStudent);
baseMapper.updateById(targetBedStudent);
log.info("班主任床位互换成功");
return true;
} catch (Exception e) {
log.error("班主任床位互换失败", e);
throw new RuntimeException("床位互换失败:" + e.getMessage());
}
}
/**
* 班主任床位移动
* 将学生从当前床位移动到指定的空闲床位
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean classTeacherMoveBed(Long sourceBedId, Long targetBedId, Long classId) {
try {
log.info("班主任床位移动开始,源床位ID: {}, 目标床位ID: {}, 班级ID: {}", sourceBedId, targetBedId, classId);
// 1. 校验参数
if (sourceBedId == null || targetBedId == null) {
throw new RuntimeException("床位ID不能为空");
}
if (sourceBedId.equals(targetBedId)) {
throw new RuntimeException("不能选择相同的床位进行移动");
}
// 2. 检查源床位是否有学生分配
LambdaQueryWrapper<XgDormBedStudent> sourceQueryWrapper = Wrappers.lambdaQuery();
sourceQueryWrapper.eq(XgDormBedStudent::getBedId, sourceBedId)
.eq(XgDormBedStudent::getDeleted, "0");
XgDormBedStudent sourceBedStudent = baseMapper.selectOne(sourceQueryWrapper);
if (sourceBedStudent == null) {
throw new RuntimeException("源床位未分配学生");
}
// 3. 验证班级权限(如果指定了班级ID)
if (classId != null && !classId.equals(sourceBedStudent.getClassId())) {
throw new RuntimeException("没有权限操作此床位");
}
// 4. 检查目标床位是否空闲
LambdaQueryWrapper<XgDormBedStudent> targetQueryWrapper = Wrappers.lambdaQuery();
targetQueryWrapper.eq(XgDormBedStudent::getBedId, targetBedId)
.eq(XgDormBedStudent::getDeleted, "0");
XgDormBedStudent targetBedStudent = baseMapper.selectOne(targetQueryWrapper);
if (targetBedStudent != null) {
throw new RuntimeException("目标床位已被占用");
}
// 5. 检查目标床位是否存在且可用
XgDormBed targetBed = xgDormBedMapper.selectById(targetBedId);
if (targetBed == null || !"0".equals(targetBed.getDeleted())) {
throw new RuntimeException("目标床位不存在");
}
if (!"0".equals(targetBed.getBedStatus())) {
throw new RuntimeException("目标床位不可用(维修中或其他状态)");
}
// 6. 创建目标床位的新分配记录
XgDormBedStudent newBedStudent = new XgDormBedStudent();
newBedStudent.setBedId(targetBedId);
newBedStudent.setStudentId(sourceBedStudent.getStudentId());
newBedStudent.setClassId(sourceBedStudent.getClassId());
newBedStudent.setCollegeId(sourceBedStudent.getCollegeId());
newBedStudent.setCreateTime(new Date());
newBedStudent.setCreateBy(LoginHelper.getUserId());
newBedStudent.setDeleted("0");
// 7. 插入新记录
baseMapper.insert(newBedStudent);
// 8. 删除源床位的分配记录
sourceBedStudent.setDeleted("1");
sourceBedStudent.setUpdateTime(new Date());
sourceBedStudent.setUpdateBy(LoginHelper.getUserId());
baseMapper.updateById(sourceBedStudent);
log.info("班主任床位移动成功");
return true;
} catch (Exception e) {
log.error("班主任床位移动失败", e);
throw new RuntimeException("床位移动失败:" + e.getMessage());
}
}
/**
* 获取班级所有已入住宿舍的学生床位信息
* 包含床位信息(校区名称、宿舍区名称、楼栋名称、房间名称、床位名称)
* 和学生信息(学生姓名、学号、性别)
*/
@Override
public List<Object> getClassOccupiedBedDetails(Long classId) {
try {
log.info("开始获取班级已入住床位详情,班级ID: {}", classId);
// 查询已分配学生的床位信息
List<Map<String, Object>> bedDetails = baseMapper.selectMaps(
Wrappers.lambdaQuery(XgDormBedStudent.class)
.select(XgDormBedStudent::getBedId, XgDormBedStudent::getStudentId,
XgDormBedStudent::getClassId, XgDormBedStudent::getCollegeId)
.eq(XgDormBedStudent::getClassId, classId)
.isNotNull(XgDormBedStudent::getStudentId)
.eq(XgDormBedStudent::getStatus, "0")
.eq(XgDormBedStudent::getDeleted, "0"));
List<Object> result = new ArrayList<>();
for (Map<String, Object> bedStudent : bedDetails) {
Long bedId = (Long) bedStudent.get("bed_id");
Long studentId = (Long) bedStudent.get("student_id");
// 查询床位详细信息
XgDormBed bed = xgDormBedMapper.selectById(bedId);
if (bed == null)
continue;
XgDormRoom room = xgDormRoomMapper.selectById(bed.getRoomId());
if (room == null)
continue;
XgDormBuilding building = xgDormBuildingMapper.selectById(room.getBuildingId());
if (building == null)
continue;
XgDormArea area = xgDormAreaMapper.selectById(building.getAreaId());
if (area == null)
continue;
XgDormCampus campus = xgDormCampusMapper.selectById(area.getCampusId());
if (campus == null)
continue;
// 查询学生信息
XgStudentMsg student = xgStudentMsgMapper.selectById(studentId);
if (student == null)
continue;
// 组装结果数据
Map<String, Object> detailInfo = new LinkedHashMap<>();
// 床位信息
detailInfo.put("bedId", bedId);
detailInfo.put("campusName", campus.getCampusName());
detailInfo.put("areaName", area.getAreaName());
detailInfo.put("buildingName", building.getBuildingName());
detailInfo.put("roomName", room.getRoomName());
detailInfo.put("bedName", bed.getBedName());
detailInfo.put("bedType", bed.getBedType());
// 床位性别类型(基于楼栋类型)
String bedGenderType = building.getBuildingType(); // 1男生楼 2女生楼 3混合楼
detailInfo.put("bedGenderType", bedGenderType);
// 学生信息
detailInfo.put("studentId", studentId);
detailInfo.put("studentName", student.getName());
detailInfo.put("studentNo", student.getStudentNo());
detailInfo.put("gender", student.getGender());
result.add(detailInfo);
}
log.info("获取班级已入住床位详情成功,共{}条记录", result.size());
return result;
} catch (Exception e) {
log.error("获取班级已入住床位详情失败", e);
return Collections.emptyList();
}
}
/**
* 获取班级所有未安排学生的床位信息
* 包含床位信息(校区名称、宿舍区名称、楼栋名称、房间名称、床位名称)
*/
@Override
public List<Object> getClassUnoccupiedBedDetails(Long classId) {
try {
log.info("开始获取班级未分配学生床位详情,班级ID: {}", classId);
// 查询分配给班级但未分配学生的床位信息
List<Map<String, Object>> bedDetails = baseMapper.selectMaps(
Wrappers.lambdaQuery(XgDormBedStudent.class)
.select(XgDormBedStudent::getBedId, XgDormBedStudent::getClassId,
XgDormBedStudent::getCollegeId)
.eq(XgDormBedStudent::getClassId, classId)
.isNull(XgDormBedStudent::getStudentId)
.eq(XgDormBedStudent::getStatus, "0")
.eq(XgDormBedStudent::getDeleted, "0"));
List<Object> result = new ArrayList<>();
for (Map<String, Object> bedStudent : bedDetails) {
Long bedId = (Long) bedStudent.get("bed_id");
// 查询床位详细信息
XgDormBed bed = xgDormBedMapper.selectById(bedId);
if (bed == null)
continue;
// 确保床位可用
if (!"1".equals(bed.getBedStatus()) || !"0".equals(bed.getDeleted())) {
continue;
}
XgDormRoom room = xgDormRoomMapper.selectById(bed.getRoomId());
if (room == null)
continue;
XgDormBuilding building = xgDormBuildingMapper.selectById(room.getBuildingId());
if (building == null)
continue;
XgDormArea area = xgDormAreaMapper.selectById(building.getAreaId());
if (area == null)
continue;
XgDormCampus campus = xgDormCampusMapper.selectById(area.getCampusId());
if (campus == null)
continue;
// 组装结果数据
Map<String, Object> detailInfo = new LinkedHashMap<>();
// 床位信息
detailInfo.put("bedId", bedId);
detailInfo.put("campusName", campus.getCampusName());
detailInfo.put("areaName", area.getAreaName());
detailInfo.put("buildingName", building.getBuildingName());
detailInfo.put("roomName", room.getRoomName());
detailInfo.put("bedName", bed.getBedName());
detailInfo.put("bedType", bed.getBedType());
// 床位性别类型(基于楼栋类型)
String bedGenderType = building.getBuildingType(); // 1男生楼 2女生楼 3混合楼
detailInfo.put("bedGenderType", bedGenderType);
result.add(detailInfo);
}
log.info("获取班级未分配学生床位详情成功,共{}条记录", result.size());
return result;
} catch (Exception e) {
log.error("获取班级未分配学生床位详情失败", e);
return Collections.emptyList();
}
}
}
@@ -215,3 +215,60 @@ export const classTeacherStayawaySelectedBeds = (bedIds: number[], classId?: str
params: { classId }
});
};
/**
* 班主任床位互换
* 将两个有学生的床位进行互换
* @param sourceBedId 源床位ID
* @param targetBedId 目标床位ID
* @param classId 班级ID,可为空
*/
export const classTeacherExchangeBeds = (sourceBedId: number, targetBedId: number, classId?: string | number) => {
return request({
url: '/dormBedStudent/dormBedStudent/classTeacherExchangeBeds',
method: 'post',
params: { sourceBedId, targetBedId, classId }
});
};
/**
* 班主任床位移动
* 将学生从当前床位移动到指定的空闲床位
* @param sourceBedId 源床位ID
* @param targetBedId 目标床位ID
* @param classId 班级ID,可为空
*/
export const classTeacherMoveBed = (sourceBedId: number, targetBedId: number, classId?: string | number) => {
return request({
url: '/dormBedStudent/dormBedStudent/classTeacherMoveBed',
method: 'post',
params: { sourceBedId, targetBedId, classId }
});
};
/**
* 获取班级所有已入住宿舍的学生床位信息
* 包含床位信息(校区名称、宿舍区名称、楼栋名称、房间名称、床位名称)
* 和学生信息(学生姓名、学号、性别)
* @param classId 班级ID,可为空
*/
export const getClassOccupiedBedDetails = (classId?: string | number) => {
return request({
url: '/dormBedStudent/dormBedStudent/getClassOccupiedBedDetails',
method: 'get',
params: { classId }
});
};
/**
* 获取班级所有未安排学生的床位信息
* 包含床位信息(校区名称、宿舍区名称、楼栋名称、房间名称、床位名称)
* @param classId 班级ID,可为空
*/
export const getClassUnoccupiedBedDetails = (classId?: string | number) => {
return request({
url: '/dormBedStudent/dormBedStudent/getClassUnoccupiedBedDetails',
method: 'get',
params: { classId }
});
};
+46
View File
@@ -100,6 +100,7 @@
<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-button type="danger" icon="House" size="small" @click="handleStayawayBeds">退宿选中床位</el-button>
<el-button type="info" icon="Switch" size="small" @click="handleBedTransfer">调宿</el-button>
</el-col>
<el-col :span="12" style="text-align: right">
<el-button
@@ -294,6 +295,17 @@
:parent-class-info="currentClassInfo"
@success="handleAllocationSuccess"
/>
<!-- 调宿管理对话框 -->
<bed-transfer-dialog
v-model="bedTransferDialog.visible"
:title="bedTransferDialog.title"
:selected-bed="bedTransferDialog.selectedBed"
:building-info="bedTransferDialog.buildingInfo"
:class-id="activeClassId"
:parent-class-info="currentClassInfo"
@success="handleAllocationSuccess"
/>
</div>
</template>
@@ -325,6 +337,7 @@ import ElMessageBox from 'element-plus/es/components/message-box/index';
// 引入子组件
import SelectiveAllocationDialog from './components/SelectiveAllocationDialog.vue';
import BedTransferDialog from './components/BedTransferDialog.vue';
// 组件状态
const dormTreeRef = ref();
@@ -411,6 +424,14 @@ const selectiveAllocationDialog = reactive({
buildingInfo: undefined as DormBuildingTreeVO | undefined
});
// 调宿对话框
const bedTransferDialog = reactive({
visible: false,
title: '调宿管理',
selectedBed: undefined as any | undefined,
buildingInfo: undefined as DormBuildingTreeVO | undefined
});
// 表格最大高度计算
const tableMaxHeight = computed(() => {
// 根据不同屏幕尺寸动态调整预留空间
@@ -814,6 +835,31 @@ const handleStayawayBeds = async () => {
}
};
/** 调宿管理 */
const handleBedTransfer = () => {
if (!selectedBuilding.value) {
ElMessage.warning('请先选择宿舍楼');
return;
}
// 获取选中的已分配床位
const selectedAllocatedBeds = selectedRows.value.filter((row) => row.isBed && row.studentId);
if (selectedAllocatedBeds.length === 0) {
ElMessage.warning('请先勾选要调宿的床位(必须是已分配学生的床位)!');
return;
}
if (selectedAllocatedBeds.length > 1) {
ElMessage.warning('调宿操作一次只能选择一个床位!');
return;
}
bedTransferDialog.selectedBed = selectedAllocatedBeds[0];
bedTransferDialog.buildingInfo = selectedBuilding.value;
bedTransferDialog.visible = true;
};
/** 分配成功回调 */
const handleAllocationSuccess = () => {
// 清除缓存,确保数据刷新
@@ -0,0 +1,702 @@
<template>
<el-dialog v-model="dialogVisible" :title="title" width="1000px" :before-close="handleClose" destroy-on-close class="bed-transfer-dialog">
<div class="transfer-container">
<!-- 当前选中床位信息 -->
<div class="current-bed-info">
<div class="info-header">
<h3 class="info-title">
<el-icon><Avatar /></el-icon>
当前选中床位
</h3>
</div>
<el-card v-if="props.selectedBed" shadow="hover" class="bed-card">
<div class="bed-info-grid">
<div class="info-item">
<span class="label">学生姓名:</span>
<span class="value">{{ props.selectedBed.studentName }}</span>
</div>
<div class="info-item">
<span class="label">学号:</span>
<span class="value">{{ props.selectedBed.studentNo }}</span>
</div>
<div class="info-item">
<span class="label">性别:</span>
<el-tag v-if="props.selectedBed.studentGender === '0'" type="primary" size="small">男</el-tag>
<el-tag v-else-if="props.selectedBed.studentGender === '1'" class="female-tag" size="small">女</el-tag>
</div>
<!-- <div class="info-item">
<span class="label">校区:</span>
<span class="value">{{ props.selectedBed.campusName || '未知' }}</span>
</div>
<div class="info-item">
<span class="label">区域:</span>
<span class="value">{{ props.selectedBed.areaName || '未知' }}</span>
</div>
<div class="info-item">
<span class="label">楼栋:</span>
<span class="value">{{ props.selectedBed.buildingName || '未知' }}</span>
</div> -->
<div class="info-item">
<span class="label">房间号:</span>
<span class="value">{{ props.selectedBed.roomName }}</span>
</div>
<div class="info-item">
<span class="label">床位号:</span>
<span class="value">{{ props.selectedBed.bedName }}</span>
</div>
<div class="info-item">
<span class="label">床位类型:</span>
<el-tag v-if="props.selectedBed.bedType === '1'" type="success" size="small">下铺</el-tag>
<el-tag v-else-if="props.selectedBed.bedType === '2'" type="warning" size="small">上铺</el-tag>
<el-tag v-else-if="props.selectedBed.bedType === '3'" type="danger" size="small">中铺</el-tag>
<el-tag v-else type="info" size="small">独立床</el-tag>
</div>
</div>
</el-card>
</div>
<!-- 调宿模式选择 -->
<div class="transfer-mode">
<div class="mode-header">
<h3 class="mode-title">
<el-icon><Switch /></el-icon>
调宿模式
</h3>
</div>
<el-radio-group v-model="transferMode" @change="handleModeChange">
<el-radio value="exchange">
<el-icon><Refresh /></el-icon>
床位互换
<span class="mode-desc">与另一个有学生的床位互换</span>
</el-radio>
<el-radio value="move">
<el-icon><Right /></el-icon>
移动床位
<span class="mode-desc">移动到一个空闲的床位</span>
</el-radio>
</el-radio-group>
</div>
<!-- 目标床位选择区域 -->
<div class="target-bed-area">
<div class="area-header">
<h3 class="area-title">
<el-icon><House /></el-icon>
目标床位选择
<el-tag v-if="transferMode === 'exchange'" type="warning" size="small">请选择有学生的床位</el-tag>
<el-tag v-else type="success" size="small">请选择空闲床位</el-tag>
<el-tag v-if="props.selectedBed?.studentGender" type="info" size="small">
已过滤:仅显示{{ props.selectedBed.studentGender === '0' ? '男生' : '女生' }}可住床位
</el-tag>
</h3>
<div class="stats">
<span v-if="selectedTargetBed"> 已选择目标床位:{{ selectedTargetBed.roomName }} - {{ selectedTargetBed.bedName }} </span>
<span v-else class="text-gray-500">请选择目标床位</span>
</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="300"
max-height="300"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:expand-row-keys="expandRowKeys"
@expand-change="handleExpandChange"
@current-change="handleTargetBedSelection"
highlight-current-row
>
<el-table-column prop="roomName" label="房间/床位" min-width="180">
<template #default="scope">
<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 label="校区" width="100" align="center">
<template #default="scope">
<span v-if="scope.row.campusName" class="text-gray-700">{{ scope.row.campusName }}</span>
<span v-else class="text-gray-400">—</span>
</template>
</el-table-column>
<el-table-column label="楼栋" width="120" align="center">
<template #default="scope">
<span v-if="scope.row.buildingName" class="text-gray-700">{{ scope.row.buildingName }}</span>
<span v-else class="text-gray-400">—</span>
</template>
</el-table-column>
<el-table-column label="类型" width="80" align="center">
<template #default="scope">
<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="scope">
<template v-if="scope.row.isBed">
<el-tag v-if="scope.row.bedGenderType === '1'" type="primary" size="small">男生</el-tag>
<el-tag v-if="scope.row.bedGenderType === '2'" type="danger" size="small">女生</el-tag>
<el-tag v-if="scope.row.bedGenderType === '3'" type="info" size="small">混合</el-tag>
</template>
<template v-else>
<el-tag v-if="scope.row.buildingType === '1'" type="primary" size="small">男生楼</el-tag>
<el-tag v-if="scope.row.buildingType === '2'" type="danger" size="small">女生楼</el-tag>
<el-tag v-if="scope.row.buildingType === '3'" type="info" size="small">混合楼</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template #default="scope">
<template v-if="scope.row.isBed">
<el-tag v-if="scope.row.studentId" type="warning" size="small">已分配</el-tag>
<el-tag v-else type="success" size="small">空闲</el-tag>
</template>
<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.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>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm" :disabled="!selectedTargetBed" :loading="confirmLoading">
确认{{ transferMode === 'exchange' ? '互换' : '移动' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, nextTick } from 'vue';
import { Avatar, House, Switch, Refresh, Right } from '@element-plus/icons-vue';
import ElMessage from 'element-plus/es/components/message/index';
import ElMessageBox from 'element-plus/es/components/message-box/index';
// 导入调宿相关的API方法
import { getClassTeacherBedDetails, classTeacherExchangeBeds, classTeacherMoveBed } from '@/api/xg/xgss/dormBedStudent/dormBedStudent';
// @ts-expect-error 新添加的API方法,类型定义可能尚未更新
import { getClassOccupiedBedDetails, getClassUnoccupiedBedDetails } from '@/api/xg/xgss/dormBedStudent/dormBedStudent';
// 定义 props
const props = defineProps<{
title?: string;
selectedBed?: any;
buildingInfo?: any;
classId?: string;
parentClassInfo?: any;
}>();
// 定义 emits
const emit = defineEmits<{
'update:modelValue': [value: boolean];
success: [];
}>();
// 定义 model
const dialogVisible = defineModel<boolean>({ default: false });
// 状态变量
const transferMode = ref<'exchange' | 'move'>('exchange'); // 调宿模式
const selectedTargetBed = ref<any>(null); // 选中的目标床位
const bedSearchKey = ref(''); // 床位搜索关键字
const bedLoading = ref(false); // 床位数据加载状态
const confirmLoading = ref(false); // 确认按钮加载状态
const expandRowKeys = ref<string[]>([]); // 展开的房间行
const bedTableRef = ref(); // 床位表格引用
// 床位数据
const bedList = ref<any[]>([]);
// 过滤后的床位列表
const filteredBedList = computed(() => {
if (!bedSearchKey.value) {
return bedList.value;
}
const keyword = bedSearchKey.value.toLowerCase();
return bedList.value.filter((room) => {
if (room.roomName.toLowerCase().includes(keyword)) {
return true;
}
if (room.children) {
const hasMatchBed = room.children.some((bed: any) => bed.bedName.toLowerCase().includes(keyword));
if (hasMatchBed) {
return true;
}
}
return false;
});
});
// 监听对话框显示状态
watch(dialogVisible, (newVal) => {
if (newVal) {
loadBedData();
} else {
resetDialog();
}
});
// 监听调宿模式变化
const handleModeChange = () => {
selectedTargetBed.value = null;
bedTableRef.value?.setCurrentRow(null);
// 根据新的调宿模式重新加载床位数据
loadBedData();
};
// 加载床位数据
const loadBedData = async () => {
if (!props.classId) return;
bedLoading.value = true;
try {
let response;
if (transferMode.value === 'exchange') {
// 互换模式:获取已分配学生的床位信息
response = await getClassOccupiedBedDetails(props.classId);
} else {
// 移动模式:获取未分配学生的床位信息
response = await getClassUnoccupiedBedDetails(props.classId);
}
let rawData = response.data || [];
// 在互换模式下,过滤掉当前选中的床位
if (transferMode.value === 'exchange' && props.selectedBed?.id) {
const originalCount = rawData.length;
rawData = rawData.filter((bed: any) => bed.bedId !== props.selectedBed.id);
const filteredCount = rawData.length;
console.log(`互换模式过滤当前床位:原始床位 ${originalCount} 个,过滤后 ${filteredCount} 个`);
}
// 根据当前学生性别过滤床位数据
const filteredData = filterBedsByGender(rawData);
// 转换数据格式为树形结构(按房间分组)
const processedData = convertToBedTreeData(filteredData);
bedList.value = processedData;
// 自动展开所有房间
await nextTick();
expandRowKeys.value = processedData.map((room: any) => String(room.id));
} catch (error) {
console.error('加载床位数据失败', error);
ElMessage.error('加载床位数据失败');
} finally {
bedLoading.value = false;
}
};
// 根据当前学生性别过滤床位数据
const filterBedsByGender = (bedDetails: any[]) => {
if (!props.selectedBed?.studentGender) {
console.warn('当前学生性别信息缺失,跳过性别过滤');
return bedDetails; // 如果没有学生性别信息,不进行过滤
}
const currentStudentGender = props.selectedBed.studentGender; // '0'男 '1'女
const originalCount = bedDetails.length;
const filteredBeds = bedDetails.filter((bed: any) => {
const bedGenderType = bed.bedGenderType; // '1'男生楼 '2'女生楼 '3'混合楼
// 混合楼的床位可以分配给任何性别的学生
if (bedGenderType === '3') {
return true;
}
// 男学生只能分配到男生楼的床位
if (currentStudentGender === '0' && bedGenderType === '1') {
return true;
}
// 女学生只能分配到女生楼的床位
if (currentStudentGender === '1' && bedGenderType === '2') {
return true;
}
return false;
});
const filteredCount = filteredBeds.length;
console.log(`性别过滤结果:原始床位 ${originalCount} 个,过滤后 ${filteredCount} 个(${currentStudentGender === '0' ? '男学生' : '女学生'})`);
return filteredBeds;
};
// 将床位详情数据转换为树形结构
const convertToBedTreeData = (bedDetails: any[]) => {
const roomMap = new Map();
bedDetails.forEach((bed: any) => {
const roomKey = `${bed.buildingName}-${bed.roomName}`;
if (!roomMap.has(roomKey)) {
roomMap.set(roomKey, {
id: `room_${bed.bedId}_${Math.random()}`, // 使用随机ID避免冲突
roomName: bed.roomName,
buildingName: bed.buildingName,
areaName: bed.areaName,
campusName: bed.campusName,
roomType: '1', // 默认标准间
buildingType: bed.bedGenderType, // 楼栋性别类型
isBed: false,
children: []
});
}
const room = roomMap.get(roomKey);
room.children.push({
id: bed.bedId,
bedName: bed.bedName,
bedType: bed.bedType || '0',
bedGenderType: bed.bedGenderType, // 床位性别类型:1男生楼 2女生楼 3混合楼
studentId: bed.studentId || null,
studentName: bed.studentName || null,
studentNo: bed.studentNo || null,
studentGender: bed.gender || null,
roomName: bed.roomName,
buildingName: bed.buildingName,
areaName: bed.areaName,
campusName: bed.campusName,
isBed: true
});
});
return Array.from(roomMap.values());
};
// 处理床位搜索
const handleBedSearch = () => {
// 搜索后自动展开所有房间
nextTick(() => {
if (filteredBedList.value.length > 0) {
expandRowKeys.value = filteredBedList.value.map((room) => String(room.id));
}
});
};
// 处理行展开
const handleExpandChange = (row: any, expanded: boolean) => {
const expandKey = String(row.id);
if (expanded) {
if (!expandRowKeys.value.includes(expandKey)) {
expandRowKeys.value.push(expandKey);
}
} else {
const index = expandRowKeys.value.indexOf(expandKey);
if (index !== -1) {
expandRowKeys.value.splice(index, 1);
}
}
};
// 处理目标床位选择
const handleTargetBedSelection = (currentRow: any) => {
if (!currentRow || !currentRow.isBed) {
selectedTargetBed.value = null;
return;
}
// 不能选择当前床位自己
if (currentRow.id === props.selectedBed?.id) {
ElMessage.warning('不能选择当前床位自己');
bedTableRef.value?.setCurrentRow(null);
selectedTargetBed.value = null;
return;
}
// 根据模式验证选择
if (transferMode.value === 'exchange') {
// 互换模式:必须选择有学生的床位
if (!currentRow.studentId) {
ElMessage.warning('互换模式下,请选择有学生的床位');
bedTableRef.value?.setCurrentRow(null);
selectedTargetBed.value = null;
return;
}
// 互换模式:验证两个学生的性别是否适合各自的目标床位
const currentStudentGender = props.selectedBed?.studentGender;
const targetStudentGender = currentRow.studentGender;
const currentBedGenderType = props.selectedBed?.bedGenderType;
const targetBedGenderType = currentRow.bedGenderType;
if (currentStudentGender && targetStudentGender && currentBedGenderType && targetBedGenderType) {
// 检查当前学生是否适合目标床位
const currentStudentCanUseTargetBed =
targetBedGenderType === '3' || // 混合楼
(currentStudentGender === '0' && targetBedGenderType === '1') || // 男学生到男生楼
(currentStudentGender === '1' && targetBedGenderType === '2'); // 女学生到女生楼
// 检查目标学生是否适合当前床位
const targetStudentCanUseCurrentBed =
currentBedGenderType === '3' || // 混合楼
(targetStudentGender === '0' && currentBedGenderType === '1') || // 男学生到男生楼
(targetStudentGender === '1' && currentBedGenderType === '2'); // 女学生到女生楼
if (!currentStudentCanUseTargetBed || !targetStudentCanUseCurrentBed) {
const currentGenderText = currentStudentGender === '0' ? '男生' : '女生';
const targetGenderText = targetStudentGender === '0' ? '男生' : '女生';
ElMessage.warning(`性别不匹配:${currentGenderText}和${targetGenderText}无法在这两个床位间互换`);
bedTableRef.value?.setCurrentRow(null);
selectedTargetBed.value = null;
return;
}
}
} else {
// 移动模式:必须选择空闲床位
if (currentRow.studentId) {
ElMessage.warning('移动模式下,请选择空闲床位');
bedTableRef.value?.setCurrentRow(null);
selectedTargetBed.value = null;
return;
}
}
selectedTargetBed.value = currentRow;
};
// 确认操作
const handleConfirm = async () => {
if (!selectedTargetBed.value) {
ElMessage.warning('请先选择目标床位');
return;
}
confirmLoading.value = true;
try {
const confirmMessage =
transferMode.value === 'exchange'
? `确认将 ${props.selectedBed.studentName}(${props.selectedBed.roomName}-${props.selectedBed.bedName})与 ${selectedTargetBed.value.studentName}(${selectedTargetBed.value.roomName}-${selectedTargetBed.value.bedName})进行床位互换吗?`
: `确认将 ${props.selectedBed.studentName}(${props.selectedBed.roomName}-${props.selectedBed.bedName})移动到 ${selectedTargetBed.value.roomName}-${selectedTargetBed.value.bedName} 吗?`;
await ElMessageBox.confirm(confirmMessage, '确认调宿', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 调用API
if (transferMode.value === 'exchange') {
await classTeacherExchangeBeds(props.selectedBed.id, selectedTargetBed.value.id, props.classId);
} else {
await classTeacherMoveBed(props.selectedBed.id, selectedTargetBed.value.id, props.classId);
}
ElMessage.success(`${transferMode.value === 'exchange' ? '互换' : '移动'}成功`);
emit('success');
handleClose();
} catch (error: any) {
if (error !== 'cancel') {
console.error('调宿操作失败', error);
ElMessage.error('调宿操作失败');
}
} finally {
confirmLoading.value = false;
}
};
// 关闭对话框
const handleClose = () => {
dialogVisible.value = false;
};
// 重置对话框
const resetDialog = () => {
transferMode.value = 'exchange';
selectedTargetBed.value = null;
bedSearchKey.value = '';
bedList.value = [];
expandRowKeys.value = [];
confirmLoading.value = false;
};
</script>
<style scoped>
.bed-transfer-dialog {
min-height: 600px;
}
.transfer-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.current-bed-info .info-header,
.transfer-mode .mode-header,
.target-bed-area .area-header {
margin-bottom: 12px;
}
.info-title,
.mode-title,
.area-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: bold;
color: var(--el-color-primary);
margin: 0;
}
.bed-card {
margin-bottom: 0;
}
.bed-info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
}
.info-item .label {
font-weight: 500;
color: #666;
white-space: nowrap;
}
.info-item .value {
font-weight: 600;
color: #333;
}
.transfer-mode {
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 16px;
background-color: #fafafa;
}
.transfer-mode :deep(.el-radio) {
display: flex;
align-items: center;
margin-bottom: 12px;
padding: 8px;
border-radius: 4px;
transition: all 0.3s;
}
.transfer-mode :deep(.el-radio:hover) {
background-color: #f0f9ff;
}
.transfer-mode :deep(.el-radio.is-checked) {
background-color: #e1f5fe;
}
.mode-desc {
color: #999;
font-size: 13px;
margin-left: 8px;
}
.target-bed-area .area-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.stats {
font-size: 14px;
color: #666;
}
.search-bar {
margin-bottom: 12px;
}
.bed-name {
font-weight: 500;
}
.female-tag {
background-color: rgba(255, 105, 180, 0.1) !important;
color: #ff69b4 !important;
border-color: rgba(255, 105, 180, 0.1) !important;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 表格样式 */
:deep(.el-table) {
border-radius: 4px;
}
:deep(.el-table .el-table__row) {
cursor: pointer;
}
:deep(.el-table .el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.el-table .current-row) {
background-color: #e6f7ff !important;
}
:deep(.el-table .current-row:hover) {
background-color: #d1eeff !important;
}
</style>