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