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