Files
hzdyjs/web/src/views/xg/xgss/xsssfpejxy/index.vue
T
2025-08-13 06:38:35 +08:00

1107 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>