1107 lines
36 KiB
Vue
1107 lines
36 KiB
Vue
<template>
|
||
<div class="p-2">
|
||
<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>
|
||
<span v-if="data.type === 'college' || data.type === 'class'" class="bed-count">
|
||
<template v-if="selectedBuilding?.buildingType === '2'">
|
||
<el-tag size="small" class="female-tag ml-2">女:{{ data.femaleAssigned }}/{{ data.femaleTotal }}</el-tag>
|
||
<el-tag size="small" type="primary" class="ml-2">男:{{ data.maleAssigned }}/{{ data.maleTotal }}</el-tag>
|
||
</template>
|
||
<template v-else>
|
||
<el-tag size="small" type="primary" class="ml-2">男:{{ data.maleAssigned }}/{{ data.maleTotal }}</el-tag>
|
||
<el-tag size="small" class="female-tag ml-2">女:{{ data.femaleAssigned }}/{{ data.femaleTotal }}</el-tag>
|
||
</template>
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</el-tree>
|
||
</el-card>
|
||
|
||
<!-- 学生树 -->
|
||
<el-card shadow="hover" class="tree-card mt-4">
|
||
<div class="card-header-title">学生信息</div>
|
||
<el-input
|
||
v-model="studentSearchKey"
|
||
prefix-icon="Search"
|
||
placeholder="请输入关键词搜索"
|
||
clearable
|
||
@clear="watchStudentSearch"
|
||
@keyup.enter="watchStudentSearch"
|
||
>
|
||
</el-input>
|
||
<el-tree
|
||
ref="studentTreeRef"
|
||
class="mt-2"
|
||
node-key="id"
|
||
:data="studentTreeData"
|
||
:props="{ label: (data) => data.label, children: 'children' }"
|
||
:expand-on-click-node="false"
|
||
:filter-node-method="filterStudentNode"
|
||
highlight-current
|
||
default-expand-all
|
||
@node-click="handleStudentNodeClick"
|
||
>
|
||
<template #default="{ node, data }">
|
||
<span class="custom-tree-node">
|
||
<span>{{ node.label }}</span>
|
||
<span v-if="data.type === 'college' || data.type === 'major' || data.type === 'class'" class="bed-count">
|
||
<template v-if="selectedBuilding?.buildingType === '2'">
|
||
<el-tag size="small" class="female-tag ml-2">女:{{ data.femaleAssigned }}/{{ data.femaleTotal }}</el-tag>
|
||
<el-tag size="small" type="primary" class="ml-2">男:{{ data.maleAssigned }}/{{ data.maleTotal }}</el-tag>
|
||
</template>
|
||
<template v-else>
|
||
<el-tag size="small" type="primary" class="ml-2">男:{{ data.maleAssigned }}/{{ data.maleTotal }}</el-tag>
|
||
<el-tag size="small" class="female-tag ml-2">女:{{ data.femaleAssigned }}/{{ data.femaleTotal }}</el-tag>
|
||
</template>
|
||
</span>
|
||
</span>
|
||
</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">请在左侧选择宿舍楼</p>
|
||
</div>
|
||
</el-card>
|
||
|
||
<template v-else>
|
||
<el-card shadow="hover" class="mb-4">
|
||
<template #header>
|
||
<el-row>
|
||
<el-col :span="12">
|
||
<el-button type="primary" icon="check" size="small" @click="handleAddRoom">分配给二级学院</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
|
||
>
|
||
<span class="text-blue-500 whitespace-nowrap">
|
||
已选择: {{ selectedRoomCount }}个房间 / {{ selectedBedCount }}个床位
|
||
<span v-if="selectedRoomCount > 0 || selectedBedCount > 0" class="text-gray-500 text-sm ml-2">
|
||
(房间选择时会自动包含其下所有空闲床位)
|
||
</span>
|
||
</span>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- <el-button type="primary" icon="Plus" @click="handleAddRoom">添加房间</el-button> -->
|
||
</div>
|
||
|
||
<!-- 房间及床位树形表格 -->
|
||
<el-table
|
||
ref="tableRef"
|
||
v-loading="loading"
|
||
: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"
|
||
>
|
||
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="120">
|
||
<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="100" align="center">
|
||
<template #default="scope">
|
||
<template v-if="!scope.row.isBed">
|
||
<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>
|
||
</template>
|
||
<template v-else>
|
||
<el-tag v-if="scope.row.bedStatus === '0'" type="success">空闲</el-tag>
|
||
<el-tag v-else-if="scope.row.bedStatus === '1'" type="warning">已分配</el-tag>
|
||
<el-tag v-else type="danger">维修中</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-column label="操作" width="180" align="center" fixed="right">
|
||
<template #default="scope">
|
||
<template v-if="!scope.row.isBed">
|
||
<!-- <el-button type="primary" link icon="Plus" @click="handleAddBed(scope.row)" /> -->
|
||
<el-button type="primary" link icon="Edit" @click="handleEditRoom(scope.row)" />
|
||
<el-button type="danger" link icon="Delete" @click="handleDeleteRoom(scope.row)" />
|
||
</template>
|
||
<template v-else>
|
||
<el-button type="primary" link icon="Edit" @click="handleEditBed(scope.row)" />
|
||
<el-button type="danger" link icon="Delete" @click="handleDeleteBed(scope.row)" />
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 房间管理对话框 -->
|
||
<room-dialog
|
||
v-model="roomDialog.visible"
|
||
:title="roomDialog.title"
|
||
:room-id="roomDialog.roomId"
|
||
:floor-num="roomDialog.floorNum"
|
||
:building-id="roomDialog.buildingId"
|
||
:room="roomDialog.room"
|
||
@success="handleRoomSuccess"
|
||
/>
|
||
|
||
<!-- 床位表单对话框 -->
|
||
<bed-dialog
|
||
v-model="bedDialog.visible"
|
||
:title="bedDialog.title"
|
||
:bed-id="bedDialog.bedId"
|
||
:room-id="bedDialog.roomId"
|
||
@success="handleBedSuccess"
|
||
/>
|
||
|
||
<!-- 学院分配对话框 -->
|
||
<college-allocation-dialog
|
||
v-model="collegeAllocationDialog.visible"
|
||
:title="collegeAllocationDialog.title"
|
||
:selected-beds="collegeAllocationDialog.selectedBeds"
|
||
:building-info="collegeAllocationDialog.buildingInfo"
|
||
@success="handleCollegeAllocationSuccess"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="DormRoomTreeManagement" lang="ts">
|
||
import { ref, computed, reactive, onMounted, nextTick, watch, getCurrentInstance } from 'vue';
|
||
import { InfoFilled } from '@element-plus/icons-vue';
|
||
import type { DormTreeNode, BedInfo } from '@/api/xg/xgss/xsssfjgl/types';
|
||
import type { DormRoomVO } from '@/api/xg/xgss/dormRoom/types';
|
||
import type { DormBedDetailVO } from '@/api/xg/xgss/dormBed/types';
|
||
import type { XgStudentTreeVO } from '@/api/xg/xsjcxx/studentMsg/types';
|
||
import { getDormTree } from '@/api/xg/xgss/xsssfjgl';
|
||
import { getBedDetailsByRoomId, delDormBed, getDormBed } from '@/api/xg/xgss/dormBed';
|
||
import { listDormRoom, delDormRoom } from '@/api/xg/xgss/dormRoom';
|
||
import { getStudentTree } from '@/api/xg/xsjcxx/studentMsg';
|
||
import ElMessage from 'element-plus/es/components/message/index';
|
||
import ElMessageBox from 'element-plus/es/components/message-box/index';
|
||
|
||
// 引入子组件
|
||
import RoomDialog from './components/RoomDialog.vue';
|
||
import BedDialog from './components/BedDialog.vue';
|
||
import CollegeAllocationDialog from './components/CollegeAllocationDialog.vue';
|
||
|
||
// const { proxy } = getCurrentInstance();
|
||
|
||
// 组件状态
|
||
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 selectedRows = ref<any[]>([]);
|
||
const selectedRoomCount = ref(0);
|
||
const selectedBedCount = ref(0);
|
||
|
||
const animate = reactive({
|
||
searchAnimate: {
|
||
enter: 'animate__animated animate__fadeInDown',
|
||
leave: 'animate__animated animate__fadeOutUp'
|
||
}
|
||
});
|
||
|
||
// 宿舍树数据
|
||
const dormOptions = ref<DormTreeNode[]>([]);
|
||
const selectedBuilding = ref<DormTreeNode>();
|
||
|
||
// 房间和床位数据(合并展示)
|
||
const roomList = ref<any[]>([]);
|
||
|
||
// 添加请求取消控制器
|
||
let currentController: AbortController | null = null;
|
||
|
||
// 查询参数
|
||
const queryParams = reactive({
|
||
pageNum: 1,
|
||
pageSize: 10000,
|
||
buildingId: undefined as string | number | undefined,
|
||
roomName: '',
|
||
roomStatus: undefined as string | undefined
|
||
});
|
||
|
||
// 房间对话框
|
||
const roomDialog = reactive({
|
||
visible: false,
|
||
title: '',
|
||
roomId: undefined as string | number | undefined,
|
||
floorNum: 1,
|
||
buildingId: '' as string | number,
|
||
room: undefined as DormRoomVO | undefined
|
||
});
|
||
|
||
// 床位表单对话框
|
||
const bedDialog = reactive({
|
||
visible: false,
|
||
title: '',
|
||
bedId: undefined as string | number | undefined,
|
||
roomId: '' as string | number
|
||
});
|
||
|
||
// 学院分配对话框
|
||
const collegeAllocationDialog = reactive({
|
||
visible: false,
|
||
title: '分配床位给二级学院',
|
||
selectedBeds: [] as any[],
|
||
buildingInfo: undefined as DormTreeNode | undefined
|
||
});
|
||
|
||
// 学生树相关数据
|
||
const studentTreeRef = ref();
|
||
const studentSearchKey = ref('');
|
||
const studentTreeData = ref<XgStudentTreeVO[]>([]);
|
||
|
||
/** 通过条件过滤节点 */
|
||
const filterNode = (value: string, data: DormTreeNode) => {
|
||
if (!value) return true;
|
||
return data.label.toLowerCase().indexOf(value.toLowerCase()) !== -1;
|
||
};
|
||
|
||
/** 通过条件过滤学生节点 */
|
||
const filterStudentNode = (value: string, data: any) => {
|
||
if (!value) return true;
|
||
return data.label.toLowerCase().indexOf(value.toLowerCase()) !== -1;
|
||
};
|
||
|
||
/** 监听宿舍楼名称变化,筛选树 */
|
||
const watchBuildingName = () => {
|
||
dormTreeRef.value?.filter(buildingName.value);
|
||
};
|
||
|
||
/** 监听学生树搜索输入变化 */
|
||
const watchStudentSearch = () => {
|
||
studentTreeRef.value?.filter(studentSearchKey.value);
|
||
};
|
||
|
||
// 监听buildingName变化,实时筛选宿舍楼
|
||
watch(buildingName, () => {
|
||
watchBuildingName();
|
||
});
|
||
|
||
/** 获取宿舍树数据 */
|
||
const getDormTreeData = async () => {
|
||
try {
|
||
const res = await getDormTree();
|
||
|
||
// 处理树节点,标记非宿舍楼节点为禁用
|
||
const processTreeNodes = (nodes: DormTreeNode[]) => {
|
||
return nodes.map((node) => {
|
||
// 如果不是宿舍楼类型,则禁用该节点
|
||
if (node.type !== 'building') {
|
||
node.disabled = true;
|
||
}
|
||
|
||
// 如果是宿舍楼但没有性别信息,添加宿舍楼性别类型
|
||
if (node.type === 'building' && !(node as any).buildingType) {
|
||
// 实际项目中应该从后端获取,这里先用随机分配,后续可以修改为从后端获取
|
||
(node as any).buildingType = Math.random() > 0.5 ? '1' : '2';
|
||
}
|
||
|
||
// 递归处理子节点
|
||
if (node.children && node.children.length > 0) {
|
||
node.children = processTreeNodes(node.children);
|
||
}
|
||
return node;
|
||
});
|
||
};
|
||
|
||
dormOptions.value = processTreeNodes(res.data);
|
||
} catch (error) {
|
||
console.error('获取宿舍树失败', error);
|
||
}
|
||
};
|
||
|
||
/** 处理节点单击事件 - 增加请求取消机制 */
|
||
const handleNodeClick = (data: DormTreeNode) => {
|
||
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 handleStudentNodeClick = (data: any) => {
|
||
console.log('学生树节点点击:', data);
|
||
// 这里可以实现学生节点点击后的相关功能
|
||
};
|
||
|
||
/** 重置查询条件 */
|
||
const resetQuery = () => {
|
||
queryFormRef.value?.resetFields();
|
||
if (selectedBuilding.value) {
|
||
queryParams.buildingId = selectedBuilding.value.id;
|
||
}
|
||
handleQuery();
|
||
};
|
||
|
||
/** 加载房间数据 - 使用新的床位详细信息API */
|
||
const loadRoomData = async () => {
|
||
if (!queryParams.buildingId) return;
|
||
|
||
loading.value = true;
|
||
const controller = currentController;
|
||
|
||
try {
|
||
// 获取房间列表
|
||
const res = await listDormRoom({
|
||
...queryParams
|
||
});
|
||
const rooms = res.rows || [];
|
||
|
||
if (controller?.signal?.aborted) return;
|
||
|
||
// 并行获取所有房间的床位详细信息
|
||
const roomPromises = rooms.map(async (room) => {
|
||
// 创建房间记录
|
||
const roomRecord = {
|
||
...room,
|
||
isBed: false,
|
||
children: []
|
||
};
|
||
|
||
try {
|
||
const bedDetailRes = await getBedDetailsByRoomId(room.id);
|
||
|
||
// 如果请求已被取消,不继续处理
|
||
if (controller?.signal?.aborted) return roomRecord;
|
||
|
||
// 将床位详细信息添加为房间的子项
|
||
if (bedDetailRes.data?.length > 0) {
|
||
roomRecord.children = bedDetailRes.data.map((bedDetail: DormBedDetailVO) => ({
|
||
...bedDetail,
|
||
isBed: true,
|
||
roomName: room.roomName // 保留房间名称,方便展示
|
||
}));
|
||
}
|
||
} catch (error: any) {
|
||
// 忽略请求被取消的错误
|
||
if (error.name !== 'AbortError') {
|
||
console.error(`获取房间 ${room.id} 的床位详细信息失败`, error);
|
||
}
|
||
}
|
||
|
||
return roomRecord;
|
||
});
|
||
|
||
// 等待所有请求完成
|
||
const tempList = await Promise.all(roomPromises);
|
||
|
||
// 如果请求已被取消,不更新UI
|
||
if (controller?.signal?.aborted) return;
|
||
|
||
roomList.value = tempList;
|
||
|
||
// 重置选择状态
|
||
selectedRows.value = [];
|
||
selectedRoomCount.value = 0;
|
||
selectedBedCount.value = 0;
|
||
|
||
// 确保表格选择状态与数据一致
|
||
nextTick(() => {
|
||
// 清除所有选择,避免不可选行被选中
|
||
tableRef.value?.clearSelection();
|
||
});
|
||
} catch (error: any) {
|
||
// 忽略请求被取消的错误
|
||
if (error.name !== 'AbortError') {
|
||
console.error('加载房间数据失败', error);
|
||
}
|
||
} finally {
|
||
// 如果是当前控制器的请求完成,则关闭加载状态
|
||
if (controller === currentController) {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
/** 分配给二级学院 */
|
||
const handleAddRoom = () => {
|
||
if (!selectedBuilding.value) {
|
||
ElMessage.warning('请先选择宿舍楼');
|
||
return;
|
||
}
|
||
|
||
// 过滤出选中的床位(只要床位,不要房间)
|
||
const selectedBeds = selectedRows.value.filter((row) => row.isBed);
|
||
|
||
if (selectedBeds.length === 0) {
|
||
ElMessage.warning('请先选择要分配的床位');
|
||
return;
|
||
}
|
||
|
||
// 准备分配对话框数据
|
||
collegeAllocationDialog.selectedBeds = selectedBeds;
|
||
collegeAllocationDialog.buildingInfo = selectedBuilding.value;
|
||
collegeAllocationDialog.visible = true;
|
||
};
|
||
|
||
/** 编辑房间 */
|
||
const handleEditRoom = (row: any) => {
|
||
roomDialog.title = '编辑房间';
|
||
roomDialog.roomId = row.id;
|
||
roomDialog.floorNum = row.floorNum;
|
||
roomDialog.buildingId = row.buildingId;
|
||
roomDialog.room = { ...row };
|
||
roomDialog.visible = true;
|
||
};
|
||
|
||
/** 删除房间 */
|
||
const handleDeleteRoom = (row: any) => {
|
||
ElMessageBox.confirm(`确定删除房间 ${row.roomName} 吗?`, '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(async () => {
|
||
try {
|
||
await delDormRoom(row.id);
|
||
ElMessage.success('删除成功');
|
||
loadRoomData(); // 刷新数据
|
||
} catch (error) {
|
||
console.error('删除房间失败', error);
|
||
}
|
||
})
|
||
.catch(() => {});
|
||
};
|
||
|
||
/** 房间操作成功回调 */
|
||
const handleRoomSuccess = () => {
|
||
loadRoomData();
|
||
};
|
||
|
||
/** 添加床位 */
|
||
const handleAddBed = (row: any) => {
|
||
bedDialog.title = '添加床位';
|
||
bedDialog.bedId = undefined;
|
||
bedDialog.roomId = row.roomId;
|
||
bedDialog.visible = true;
|
||
};
|
||
|
||
/** 编辑床位 */
|
||
const handleEditBed = (row: any) => {
|
||
bedDialog.title = '编辑床位';
|
||
bedDialog.bedId = row.id;
|
||
bedDialog.roomId = row.roomId;
|
||
bedDialog.visible = true;
|
||
};
|
||
|
||
/** 删除床位 */
|
||
const handleDeleteBed = (row: any) => {
|
||
ElMessageBox.confirm(`确定删除床位 ${row.bedName} 吗?`, '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(async () => {
|
||
try {
|
||
await delDormBed(row.id);
|
||
ElMessage.success('删除成功');
|
||
loadRoomData(); // 刷新数据
|
||
} catch (error) {
|
||
console.error('删除床位失败', error);
|
||
}
|
||
})
|
||
.catch(() => {});
|
||
};
|
||
|
||
/** 床位操作成功回调 */
|
||
const handleBedSuccess = () => {
|
||
loadRoomData();
|
||
};
|
||
|
||
/** 学院分配成功回调 */
|
||
const handleCollegeAllocationSuccess = () => {
|
||
// 刷新房间和床位数据
|
||
loadRoomData();
|
||
// 清空选择状态
|
||
selectedRows.value = [];
|
||
selectedRoomCount.value = 0;
|
||
selectedBedCount.value = 0;
|
||
};
|
||
|
||
/** 处理行的展开/收起 */
|
||
const handleExpandChange = (row: any, expanded: boolean) => {
|
||
const rowId = String(row.roomId);
|
||
if (expanded) {
|
||
// 添加到已展开行数组
|
||
expandedRowKeys.value.push(rowId);
|
||
} else {
|
||
// 从已展开行数组中移除
|
||
const index = expandedRowKeys.value.findIndex((key) => key === rowId);
|
||
if (index !== -1) {
|
||
expandedRowKeys.value.splice(index, 1);
|
||
}
|
||
}
|
||
};
|
||
|
||
/** 检查行是否可选中(只有正常状态的房间和空闲状态的床位可选) */
|
||
const checkSelectable = (row: any) => {
|
||
if (row.isBed) {
|
||
// 床位:只有空闲状态(0)可选,且其父房间必须是正常状态
|
||
const parentRoom = findParentRoom(row);
|
||
return row.bedStatus === '0' && parentRoom && parentRoom.roomStatus === '0';
|
||
} else {
|
||
// 房间:只有正常状态(0)可选
|
||
return row.roomStatus === '0';
|
||
}
|
||
};
|
||
|
||
/** 查找床位的父房间 */
|
||
const findParentRoom = (bed: any) => {
|
||
return roomList.value.find((room) => room.children && room.children.some((child: any) => child.id === bed.id));
|
||
};
|
||
|
||
/** 处理全选事件 */
|
||
const handleSelectAll = (selection: any[]) => {
|
||
// 全选时,只选择可用的行
|
||
const availableRows = roomList.value.filter((row) => checkSelectable(row));
|
||
|
||
console.log('全选处理:', {
|
||
总行数: roomList.value.length,
|
||
可选行数: availableRows.length,
|
||
不可选行数: roomList.value.length - availableRows.length,
|
||
不可选行详情: roomList.value
|
||
.filter((row) => !checkSelectable(row))
|
||
.map((row) => ({
|
||
id: row.id,
|
||
name: row.isBed ? row.bedName : row.roomName,
|
||
type: row.isBed ? '床位' : '房间',
|
||
status: row.isBed ? row.bedStatus : row.roomStatus,
|
||
parentStatus: row.isBed ? findParentRoom(row)?.roomStatus : null
|
||
}))
|
||
});
|
||
|
||
// 清空当前选择
|
||
tableRef.value?.clearSelection();
|
||
|
||
// 只选择可用的行
|
||
availableRows.forEach((row) => {
|
||
tableRef.value?.toggleRowSelection(row, true);
|
||
});
|
||
};
|
||
|
||
/** 更新选择统计 */
|
||
const updateSelectionCount = (rows: any[]) => {
|
||
// 计算选中的房间和床位数量
|
||
// 注意:由于房间选择时会自动选择其下所有空闲床位,所以需要去重统计
|
||
const uniqueRooms = new Set();
|
||
const uniqueBeds = new Set();
|
||
|
||
rows.forEach((row) => {
|
||
if (row.isBed) {
|
||
// 床位:记录床位ID
|
||
uniqueBeds.add(row.id);
|
||
} else {
|
||
// 房间:记录房间ID
|
||
uniqueRooms.add(row.id);
|
||
}
|
||
});
|
||
|
||
// 统计选中的房间数(只统计直接选中的房间,不包括通过床位间接选中的房间)
|
||
selectedRoomCount.value = uniqueRooms.size;
|
||
|
||
// 统计选中的床位数(包括直接选中的床位和通过房间选择自动选中的床位)
|
||
selectedBedCount.value = uniqueBeds.size;
|
||
|
||
// 调试信息(开发时可以打开,生产环境建议注释掉)
|
||
console.log('选择统计更新:', {
|
||
总行数: rows.length,
|
||
房间数: uniqueRooms.size,
|
||
床位数: uniqueBeds.size,
|
||
房间IDs: Array.from(uniqueRooms),
|
||
床位IDs: Array.from(uniqueBeds)
|
||
});
|
||
};
|
||
|
||
/** 处理选择变更 */
|
||
const handleSelectionChange = (rows: any[]) => {
|
||
// 更新选中行数组
|
||
selectedRows.value = rows;
|
||
|
||
// 更新统计
|
||
updateSelectionCount(rows);
|
||
};
|
||
|
||
/** 当选择房间时,自动选择其下所有空闲床位 */
|
||
const toggleRoomSelection = async (room: any, isSelected: boolean) => {
|
||
if (!room || !room.children || room.children.length === 0) return;
|
||
|
||
// 获取该房间下所有空闲状态的床位,且房间必须是正常状态
|
||
const availableBeds = room.children.filter((bed: any) => bed.bedStatus === '0' && room.roomStatus === '0');
|
||
|
||
// 根据房间选择状态,选中或取消选中所有符合条件的床位
|
||
availableBeds.forEach((bed: any) => {
|
||
tableRef.value?.toggleRowSelection(bed, isSelected);
|
||
});
|
||
|
||
// 注意:这里不需要手动更新统计,因为 toggleRowSelection 会触发 handleSelectionChange
|
||
// 而 handleSelectionChange 会调用 updateSelectionCount 来更新统计
|
||
};
|
||
|
||
// 监听房间选择变化
|
||
watch(
|
||
selectedRows,
|
||
(newRows, oldRows) => {
|
||
// 找出新增的房间和移除的房间
|
||
const oldRooms = oldRows.filter((row) => !row.isBed);
|
||
const newRooms = newRows.filter((row) => !row.isBed);
|
||
|
||
// 找出新增选中的房间
|
||
const addedRooms = newRooms.filter((newRoom) => !oldRooms.some((oldRoom) => oldRoom.id === newRoom.id));
|
||
|
||
// 找出取消选中的房间
|
||
const removedRooms = oldRooms.filter((oldRoom) => !newRooms.some((newRoom) => newRoom.id === oldRoom.id));
|
||
|
||
// 为新增选中的房间选中所有空闲床位
|
||
addedRooms.forEach((room) => {
|
||
toggleRoomSelection(room, true);
|
||
});
|
||
|
||
// 为取消选中的房间取消选中所有床位
|
||
removedRooms.forEach((room) => {
|
||
toggleRoomSelection(room, false);
|
||
});
|
||
|
||
// 手动更新统计,确保统计准确
|
||
updateSelectionCount(newRows);
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
// 获取学生信息树数据
|
||
const getStudentTreeData = async () => {
|
||
try {
|
||
const res = await getStudentTree(101); // 默认使用二级学院ID 101
|
||
if (res.code === 200) {
|
||
studentTreeData.value = res.data || [];
|
||
} else {
|
||
ElMessage.error(res.msg || '获取学生信息树失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取学生信息树失败:', error);
|
||
ElMessage.error('获取学生信息树失败');
|
||
}
|
||
};
|
||
|
||
// 组件挂载时获取宿舍树数据和学生树数据
|
||
onMounted(() => {
|
||
getDormTreeData();
|
||
getStudentTreeData();
|
||
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(50vh - 24px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.tree-card :deep(.el-card__body) {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
overflow-x: auto; /* 添加横向滚动 */
|
||
padding-top: 10px;
|
||
}
|
||
|
||
.content-area {
|
||
flex-grow: 1;
|
||
overflow: visible; /* 允许表格正常显示 */
|
||
min-width: 0; /* 防止flex子项溢出 */
|
||
}
|
||
|
||
.room-container {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
/* 表格容器样式优化 */
|
||
.el-card {
|
||
overflow: visible !important;
|
||
}
|
||
|
||
/* 确保表格列宽度合理分配 */
|
||
.el-table :deep(.el-table__cell) {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* 固定列样式优化 */
|
||
.el-table :deep(.el-table__fixed-right) {
|
||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.empty-tip {
|
||
height: 100%;
|
||
}
|
||
|
||
/* 修复表格横向滚动样式 - 解决双滚动条问题 */
|
||
.el-card :deep(.el-table) {
|
||
width: 100% !important;
|
||
overflow: visible !important;
|
||
}
|
||
|
||
.el-card :deep(.el-table__body-wrapper) {
|
||
overflow-x: auto !important;
|
||
overflow-y: auto !important;
|
||
}
|
||
|
||
/* 确保表格头部不出现横向滚动条 */
|
||
.el-card :deep(.el-table__header-wrapper) {
|
||
overflow: visible !important;
|
||
}
|
||
|
||
/* 设置表格内容区域的最小宽度,避免右侧空白 */
|
||
.el-card :deep(.el-table__body),
|
||
.el-card :deep(.el-table__header) {
|
||
min-width: 100%;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 确保表格容器正确处理滚动 */
|
||
.el-card :deep(.el-table__inner-wrapper) {
|
||
overflow: visible !important;
|
||
}
|
||
|
||
/* 统计信息区域样式优化 */
|
||
.flex-layout .content-area .el-card .mb-4 .flex {
|
||
min-width: 0;
|
||
}
|
||
|
||
.flex-layout .content-area .el-card .mb-4 .flex .font-bold {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.flex-layout .content-area .el-card .mb-4 .flex .text-blue-500 {
|
||
flex-shrink: 0;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* 确保说明文字不换行 */
|
||
.flex-layout .content-area .el-card .mb-4 .flex .text-gray-500 {
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 学生树自定义样式 */
|
||
.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; /* 确保内容不被压缩 */
|
||
}
|
||
|
||
.bed-count {
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 学生树横向和纵向滚动样式 - 修复版 */
|
||
.tree-card.mt-4 {
|
||
position: relative;
|
||
}
|
||
|
||
/* 学生树卡片内容区域的滚动行为 - 使用嵌套滚动容器解决滚动条问题 */
|
||
.tree-card.mt-4 :deep(.el-card__body) {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 10px !important;
|
||
max-height: calc(50vh - 24px); /* 设置最大高度 */
|
||
overflow: hidden; /* 避免双重滚动条 */
|
||
}
|
||
|
||
/* 创建一个包装容器进行纵向滚动 */
|
||
.tree-card.mt-4 :deep(.el-input) {
|
||
flex-shrink: 0; /* 防止搜索框被压缩 */
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* 添加自定义滚动容器 */
|
||
.tree-card.mt-4 :deep(.el-tree) {
|
||
overflow-y: auto; /* 纵向滚动 */
|
||
overflow-x: auto; /* 横向滚动 */
|
||
max-width: 100%; /* 确保不超出父容器 */
|
||
min-width: 100%; /* 确保至少占满容器宽度 */
|
||
padding-bottom: 10px; /* 为横向滚动条留出空间 */
|
||
display: inline-block; /* 使容器能够根据内容扩展 */
|
||
width: max-content; /* 允许根据内容扩展宽度 */
|
||
}
|
||
|
||
/* 确保学生树节点内容完整显示 */
|
||
.tree-card.mt-4 :deep(.el-tree-node) {
|
||
min-width: max-content;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tree-card.mt-4 :deep(.el-tree-node__content) {
|
||
min-width: max-content;
|
||
width: auto !important;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 宿舍树横向和纵向滚动样式 - 修复版 */
|
||
.tree-card:first-child {
|
||
position: relative;
|
||
}
|
||
|
||
/* 宿舍树卡片内容区域的滚动行为 - 使用嵌套滚动容器解决滚动条问题 */
|
||
.tree-card:first-child :deep(.el-card__body) {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 10px !important;
|
||
max-height: calc(50vh - 24px); /* 设置最大高度 */
|
||
overflow: hidden; /* 避免双重滚动条 */
|
||
}
|
||
|
||
/* 创建一个包装容器进行纵向滚动 */
|
||
.tree-card:first-child :deep(.el-input) {
|
||
flex-shrink: 0; /* 防止搜索框被压缩 */
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* 添加自定义滚动容器 */
|
||
.tree-card:first-child :deep(.el-tree) {
|
||
overflow-y: auto; /* 纵向滚动 */
|
||
overflow-x: auto; /* 横向滚动 */
|
||
max-width: 100%; /* 确保不超出父容器 */
|
||
min-width: 100%; /* 确保至少占满容器宽度 */
|
||
padding-bottom: 10px; /* 为横向滚动条留出空间 */
|
||
display: inline-block; /* 使容器能够根据内容扩展 */
|
||
width: max-content; /* 允许根据内容扩展宽度 */
|
||
}
|
||
|
||
/* 确保宿舍树节点内容完整显示 */
|
||
.tree-card:first-child :deep(.el-tree-node) {
|
||
min-width: max-content;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tree-card:first-child :deep(.el-tree-node__content) {
|
||
min-width: max-content;
|
||
width: auto !important;
|
||
}
|
||
</style>
|