|
|
@@ -0,0 +1,989 @@
|
|
|
+<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">
|
|
|
+ <el-tag size="small" type="success" class="ml-2">男:{{ data.maleAssigned }}/{{ data.maleTotal }}</el-tag>
|
|
|
+ <el-tag size="small" type="danger" class="ml-2">女:{{ data.femaleAssigned }}/{{ data.femaleTotal }}</el-tag>
|
|
|
+ </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 === 'class'" class="bed-count">
|
|
|
+ <el-tag size="small" type="success" class="ml-2">男:{{ data.maleAssigned }}/{{ data.maleTotal }}</el-tag>
|
|
|
+ <el-tag size="small" type="danger" class="ml-2">女:{{ data.femaleAssigned }}/{{ data.femaleTotal }}</el-tag>
|
|
|
+ </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="12">
|
|
|
+ <span class="font-bold">{{ selectedBuilding?.label }} - 房间管理</span>
|
|
|
+ <span class="ml-4 text-blue-500">已选择: {{ selectedRoomCount }}个房间 / {{ selectedBedCount }}个床位</span>
|
|
|
+ </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' }"
|
|
|
+ style="width: 100%"
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
|
|
+ <el-table-column prop="roomName" label="房间/宿舍名称" min-width="180">
|
|
|
+ <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 prop="remark" label="备注" min-width="150" :show-overflow-tooltip="true">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ scope.row.remark }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="180" align="center">
|
|
|
+ <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"
|
|
|
+ />
|
|
|
+ </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/xgss/xsssfjgl/types';
|
|
|
+import type { DormRoomVO } from '@/api/xgss/dormRoom/types';
|
|
|
+import { getDormTree } from '@/api/xgss/xsssfjgl';
|
|
|
+import { listDormBed, delDormBed, getDormBed } from '@/api/xgss/dormBed';
|
|
|
+import { listDormRoom, delDormRoom } from '@/api/xgss/dormRoom';
|
|
|
+
|
|
|
+// 引入子组件
|
|
|
+import RoomDialog from './components/RoomDialog.vue';
|
|
|
+import BedDialog from './components/BedDialog.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[]>([]);
|
|
|
+
|
|
|
+// 查询参数
|
|
|
+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 studentTreeRef = ref();
|
|
|
+const studentSearchKey = ref('');
|
|
|
+const studentTreeData = ref<any[]>([
|
|
|
+ {
|
|
|
+ id: 'grade-2025',
|
|
|
+ label: '2025级',
|
|
|
+ type: 'grade',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'college-cs-2025',
|
|
|
+ label: '计算机学院',
|
|
|
+ type: 'college',
|
|
|
+ maleAssigned: 45,
|
|
|
+ maleTotal: 120,
|
|
|
+ femaleAssigned: 25,
|
|
|
+ femaleTotal: 60,
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'class-cs-1-2025',
|
|
|
+ label: '计算机科学与技术1班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 20,
|
|
|
+ maleTotal: 40,
|
|
|
+ femaleAssigned: 10,
|
|
|
+ femaleTotal: 15
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'class-cs-2-2025',
|
|
|
+ label: '计算机科学与技术2班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 25,
|
|
|
+ maleTotal: 40,
|
|
|
+ femaleAssigned: 15,
|
|
|
+ femaleTotal: 20
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'college-ee-2025',
|
|
|
+ label: '电子工程学院',
|
|
|
+ type: 'college',
|
|
|
+ maleAssigned: 55,
|
|
|
+ maleTotal: 100,
|
|
|
+ femaleAssigned: 35,
|
|
|
+ femaleTotal: 50,
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'class-ee-1-2025',
|
|
|
+ label: '电子信息工程1班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 30,
|
|
|
+ maleTotal: 50,
|
|
|
+ femaleAssigned: 20,
|
|
|
+ femaleTotal: 25
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'class-ee-2-2025',
|
|
|
+ label: '电子信息工程2班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 25,
|
|
|
+ maleTotal: 50,
|
|
|
+ femaleAssigned: 15,
|
|
|
+ femaleTotal: 25
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'grade-2024',
|
|
|
+ label: '2024级',
|
|
|
+ type: 'grade',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'college-cs-2024',
|
|
|
+ label: '计算机学院',
|
|
|
+ type: 'college',
|
|
|
+ maleAssigned: 110,
|
|
|
+ maleTotal: 120,
|
|
|
+ femaleAssigned: 58,
|
|
|
+ femaleTotal: 60,
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'class-cs-1-2024',
|
|
|
+ label: '计算机科学与技术1班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 38,
|
|
|
+ maleTotal: 40,
|
|
|
+ femaleAssigned: 14,
|
|
|
+ femaleTotal: 15
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'class-cs-2-2024',
|
|
|
+ label: '计算机科学与技术2班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 37,
|
|
|
+ maleTotal: 40,
|
|
|
+ femaleAssigned: 18,
|
|
|
+ femaleTotal: 20
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'grade-2024',
|
|
|
+ label: '2024级',
|
|
|
+ type: 'grade',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'college-cs-2024',
|
|
|
+ label: '计算机学院',
|
|
|
+ type: 'college',
|
|
|
+ maleAssigned: 110,
|
|
|
+ maleTotal: 120,
|
|
|
+ femaleAssigned: 58,
|
|
|
+ femaleTotal: 60,
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'class-cs-1-2024',
|
|
|
+ label: '计算机科学与技术1班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 38,
|
|
|
+ maleTotal: 40,
|
|
|
+ femaleAssigned: 14,
|
|
|
+ femaleTotal: 15
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'class-cs-2-2024',
|
|
|
+ label: '计算机科学与技术2班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 37,
|
|
|
+ maleTotal: 40,
|
|
|
+ femaleAssigned: 18,
|
|
|
+ femaleTotal: 20
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'grade-2024',
|
|
|
+ label: '2024级',
|
|
|
+ type: 'grade',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'college-cs-2024',
|
|
|
+ label: '计算机学院',
|
|
|
+ type: 'college',
|
|
|
+ maleAssigned: 110,
|
|
|
+ maleTotal: 120,
|
|
|
+ femaleAssigned: 58,
|
|
|
+ femaleTotal: 60,
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ id: 'class-cs-1-2024',
|
|
|
+ label: '计算机科学与技术1班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 38,
|
|
|
+ maleTotal: 40,
|
|
|
+ femaleAssigned: 14,
|
|
|
+ femaleTotal: 15
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'class-cs-2-2024',
|
|
|
+ label: '计算机科学与技术2班',
|
|
|
+ type: 'class',
|
|
|
+ maleAssigned: 37,
|
|
|
+ maleTotal: 40,
|
|
|
+ femaleAssigned: 18,
|
|
|
+ femaleTotal: 20
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]);
|
|
|
+
|
|
|
+/** 通过条件过滤节点 */
|
|
|
+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.buildingType) {
|
|
|
+ // 实际项目中应该从后端获取,这里先用随机分配,后续可以修改为从后端获取
|
|
|
+ node.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;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置选中的宿舍楼
|
|
|
+ selectedBuilding.value = data;
|
|
|
+ // 更新查询参数
|
|
|
+ queryParams.buildingId = data.id;
|
|
|
+ // 加载房间数据
|
|
|
+ handleQuery();
|
|
|
+};
|
|
|
+
|
|
|
+/** 学生树节点点击处理 */
|
|
|
+const handleStudentNodeClick = (data: any) => {
|
|
|
+ console.log('学生树节点点击:', data);
|
|
|
+ // 这里可以实现学生节点点击后的相关功能
|
|
|
+};
|
|
|
+
|
|
|
+/** 查询房间数据 */
|
|
|
+const handleQuery = () => {
|
|
|
+ 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;
|
|
|
+ try {
|
|
|
+ // 获取房间列表
|
|
|
+ const res = await listDormRoom(queryParams);
|
|
|
+ const rooms = res.rows || [];
|
|
|
+
|
|
|
+ // 存储临时结果
|
|
|
+ const tempList: any[] = [];
|
|
|
+
|
|
|
+ // 处理每个房间
|
|
|
+ for (const room of rooms) {
|
|
|
+ // 添加房间记录
|
|
|
+ const roomRecord = {
|
|
|
+ ...room,
|
|
|
+ isBed: false,
|
|
|
+ children: []
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取房间的床位信息
|
|
|
+ try {
|
|
|
+ const bedRes = await listDormBed({ roomId: room.id });
|
|
|
+ const beds = bedRes.rows || [];
|
|
|
+
|
|
|
+ // 将床位信息添加为房间的子项
|
|
|
+ if (beds.length > 0) {
|
|
|
+ roomRecord.children = beds.map((bed: any) => ({
|
|
|
+ ...bed,
|
|
|
+ isBed: true,
|
|
|
+ roomName: room.roomName // 保留房间名称,方便展示
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`获取房间 ${room.id} 的床位信息失败`, error);
|
|
|
+ }
|
|
|
+
|
|
|
+ tempList.push(roomRecord);
|
|
|
+ }
|
|
|
+
|
|
|
+ roomList.value = tempList;
|
|
|
+
|
|
|
+ // 重置选择状态
|
|
|
+ selectedRows.value = [];
|
|
|
+ selectedRoomCount.value = 0;
|
|
|
+ selectedBedCount.value = 0;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载房间数据失败', error);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 添加房间 */
|
|
|
+const handleAddRoom = () => {
|
|
|
+ if (!selectedBuilding.value) {
|
|
|
+ // 使用全局自动导入的 ElMessage
|
|
|
+ proxy.$modal.msgWarning('请先选择宿舍楼');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ roomDialog.title = '添加房间';
|
|
|
+ roomDialog.roomId = undefined;
|
|
|
+ roomDialog.floorNum = 1;
|
|
|
+ roomDialog.buildingId = selectedBuilding.value.id;
|
|
|
+ roomDialog.room = undefined;
|
|
|
+ roomDialog.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) => {
|
|
|
+ proxy.$modal
|
|
|
+ .confirm(`确定删除房间 ${row.roomName} 吗?`, '警告', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ await delDormRoom(row.id);
|
|
|
+ proxy.$modal.msgSuccess('删除成功');
|
|
|
+ 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) => {
|
|
|
+ proxy.$modal
|
|
|
+ .confirm(`确定删除床位 ${row.bedName} 吗?`, '警告', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ await delDormBed(row.id);
|
|
|
+ proxy.$modal.msgSuccess('删除成功');
|
|
|
+ loadRoomData(); // 刷新数据
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除床位失败', error);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {});
|
|
|
+};
|
|
|
+
|
|
|
+/** 床位操作成功回调 */
|
|
|
+const handleBedSuccess = () => {
|
|
|
+ loadRoomData();
|
|
|
+};
|
|
|
+
|
|
|
+/** 处理行的展开/收起 */
|
|
|
+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)可选
|
|
|
+ return row.bedStatus === '0';
|
|
|
+ } else {
|
|
|
+ // 房间:只有正常状态(0)可选
|
|
|
+ return row.roomStatus === '0';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 处理选择变更 */
|
|
|
+const handleSelectionChange = (rows: any[]) => {
|
|
|
+ // 更新选中行数组
|
|
|
+ selectedRows.value = rows;
|
|
|
+
|
|
|
+ // 计算选中的房间和床位数量
|
|
|
+ const rooms = rows.filter((row) => !row.isBed);
|
|
|
+ const beds = rows.filter((row) => row.isBed);
|
|
|
+
|
|
|
+ selectedRoomCount.value = rooms.length;
|
|
|
+ selectedBedCount.value = beds.length;
|
|
|
+};
|
|
|
+
|
|
|
+/** 当选择房间时,自动选择其下所有空闲床位 */
|
|
|
+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');
|
|
|
+
|
|
|
+ // 根据房间选择状态,选中或取消选中所有空闲床位
|
|
|
+ availableBeds.forEach((bed: any) => {
|
|
|
+ tableRef.value?.toggleRowSelection(bed, isSelected);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 监听房间选择变化
|
|
|
+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);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+);
|
|
|
+
|
|
|
+// 组件挂载时获取宿舍树数据
|
|
|
+onMounted(() => {
|
|
|
+ getDormTreeData();
|
|
|
+});
|
|
|
+</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: hidden; /* 防止溢出 */
|
|
|
+}
|
|
|
+
|
|
|
+.room-container {
|
|
|
+ margin-top: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-tip {
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 添加表格横向滚动样式 */
|
|
|
+.el-card :deep(.el-table) {
|
|
|
+ width: 100%;
|
|
|
+ overflow-x: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.el-card :deep(.el-table__body-wrapper),
|
|
|
+.el-card :deep(.el-table__header-wrapper) {
|
|
|
+ overflow-x: auto !important;
|
|
|
+}
|
|
|
+
|
|
|
+.el-card :deep(.el-table__body),
|
|
|
+.el-card :deep(.el-table__header) {
|
|
|
+ min-width: 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; /* 确保内容不被压缩 */
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+/* 宿舍树横向和纵向滚动样式 - 修复版 */
|
|
|
+.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>
|