提交代码
This commit is contained in:
1 parent
1db2f91af1
commit
574660ce9f
20 files changed
+2790
-144
No files matched your search
@@ -94,6 +94,18 @@ export const getBedDetailsByBuildingId = (buildingId: string | number): AxiosPro
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询指定房间中属于指定班级的床位详细信息列表(包含学生信息)
|
||||
* @param roomId 房间ID
|
||||
* @param classId 班级ID
|
||||
*/
|
||||
export const getBedDetailsByRoomIdAndClassId = (roomId: string | number, classId: string | number): AxiosPromise<DormBedDetailVO[]> => {
|
||||
return request({
|
||||
url: `/dormbed/dormBed/details/room/${roomId}/class/${classId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量分配床位给二级学院
|
||||
* @param data 批量分配参数
|
||||
|
||||
@@ -64,6 +64,32 @@ export const updateDormRoom = (data: DormRoomForm) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询指定班级的宿舍房间信息列表
|
||||
* @param classId 班级ID
|
||||
* @param query 查询条件
|
||||
* @returns {*}
|
||||
*/
|
||||
export const listDormRoomByClassId = (classId: string | number, query?: DormRoomQuery): AxiosPromise<DormRoomVO[]> => {
|
||||
return request({
|
||||
url: `/xgss/dormRoom/class/${classId}/list`,
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询指定房间的床位详细信息列表(包含学生信息)
|
||||
* @param roomId 房间ID
|
||||
* @returns {*}
|
||||
*/
|
||||
export const getDormBedDetailsByRoomId = (roomId: string | number): AxiosPromise<any[]> => {
|
||||
return request({
|
||||
url: `/dormbed/dormBed/details/room/${roomId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除宿舍房间信息
|
||||
* @param id
|
||||
|
||||
@@ -0,0 +1,685 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<div class="flex-layout">
|
||||
<!-- 左侧:宿舍树(不含学生树) -->
|
||||
<div class="tree-sidebar">
|
||||
<!-- 班级切换 Tabs -->
|
||||
<el-card shadow="hover" class="tree-card">
|
||||
<template #header>
|
||||
<div class="card-header-title">我的班级</div>
|
||||
</template>
|
||||
<el-tabs v-model="activeClassId" type="card" @tab-click="handleClassTabChange">
|
||||
<el-tab-pane v-for="cls in myClassList" :key="String(cls.id)" :label="`${cls.className}`" :name="String(cls.id)" />
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<!-- 宿舍楼树 -->
|
||||
<el-card shadow="hover" class="tree-card mt-4">
|
||||
<div class="card-header-title">宿舍楼</div>
|
||||
<el-input
|
||||
v-model="buildingName"
|
||||
prefix-icon="Search"
|
||||
placeholder="请输入宿舍楼名称"
|
||||
clearable
|
||||
@clear="watchBuildingName"
|
||||
@keyup.enter="watchBuildingName"
|
||||
/>
|
||||
<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="!activeClassId" 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>
|
||||
|
||||
<el-card v-else-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="openSelectiveAllocation">分配给学生</el-button>
|
||||
<el-button type="success" icon="Refresh" size="small" @click="handleRandomAllocation" :loading="actionLoading"
|
||||
>一键随机分配</el-button
|
||||
>
|
||||
<el-button type="primary" icon="Sort" size="small" @click="handleStudentNoAllocation" :loading="actionLoading"
|
||||
>按学号分配</el-button
|
||||
>
|
||||
<el-button type="warning" icon="RefreshLeft" size="small" @click="handleRecall" :loading="actionLoading">一键收回</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right">
|
||||
<el-button link type="primary" size="small" :loading="expandAllLoading" :disabled="expandAllLoading || loading" @click="expandAll"
|
||||
>全部展开</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="collapseAllLoading"
|
||||
:disabled="collapseAllLoading || 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"> 已选择: {{ 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>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 房间及床位树形表格(仅床位可选) -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading || expandAllLoading || collapseAllLoading"
|
||||
element-loading-text="处理中..."
|
||||
:data="filteredRoomList"
|
||||
row-key="id"
|
||||
border
|
||||
:expand-row-keys="expandedRowKeys"
|
||||
@expand-change="handleExpandChange"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
table-layout="auto"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<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.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="selectiveDialog.visible"
|
||||
:title="selectiveDialog.title"
|
||||
:building-info="selectedBuilding"
|
||||
:class-id="activeClassId"
|
||||
@success="handleAllocationSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ClassTeacherBedAllocation">
|
||||
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import type { DormBuildingTreeVO } from '@/api/xg/xgss/dormBedStudent/dormBedStudent/types';
|
||||
import {
|
||||
getClassTeacherDormTree,
|
||||
classTeacherRandomAllocation,
|
||||
classTeacherStudentNoAllocation,
|
||||
classTeacherRecallBeds
|
||||
} from '@/api/xg/xgss/dormBedStudent/dormBedStudent';
|
||||
import { listDormRoomByClassId, getDormBedDetailsByRoomId } from '@/api/xg/xgss/dormRoom';
|
||||
import type { ClassMsgVO } from '@/api/xg/xsjcxx/classMsg/types';
|
||||
import { getMyClassListAsTeacher } from '@/api/xg/xsjcxx/classMsg/classMsg';
|
||||
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 expandAllLoading = ref(false);
|
||||
const collapseAllLoading = ref(false);
|
||||
const actionLoading = ref(false);
|
||||
const expandedRowKeys = ref<string[]>([]);
|
||||
|
||||
const animate = reactive({
|
||||
searchAnimate: {
|
||||
enter: 'animate__animated animate__fadeInDown',
|
||||
leave: 'animate__animated animate__fadeOutUp'
|
||||
}
|
||||
});
|
||||
|
||||
// Tabs 班级
|
||||
const myClassList = ref<ClassMsgVO[]>([]);
|
||||
const activeClassId = ref<string>('');
|
||||
|
||||
// 宿舍树数据
|
||||
const dormOptions = ref<DormBuildingTreeVO[]>([]);
|
||||
const selectedBuilding = ref<DormBuildingTreeVO>();
|
||||
|
||||
// 房间/床位
|
||||
const roomList = ref<any[]>([]);
|
||||
|
||||
// 查询参数(前端过滤)
|
||||
const queryParams = reactive({
|
||||
roomName: '',
|
||||
roomStatus: undefined as string | undefined
|
||||
});
|
||||
|
||||
// 选择统计
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const selectedBedCount = ref(0);
|
||||
const selectedBedStats = ref({
|
||||
available: 0,
|
||||
allocated: 0,
|
||||
maintenance: 0
|
||||
});
|
||||
|
||||
/** 通过条件过滤节点 */
|
||||
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);
|
||||
};
|
||||
watch(buildingName, watchBuildingName);
|
||||
|
||||
/** 班级切换 */
|
||||
const handleClassTabChange = async () => {
|
||||
selectedBuilding.value = undefined;
|
||||
roomList.value = [];
|
||||
await loadDormTree();
|
||||
};
|
||||
|
||||
/** 获取我的班级(班主任) */
|
||||
const loadMyClassList = async () => {
|
||||
try {
|
||||
const res = await getMyClassListAsTeacher();
|
||||
myClassList.value = (res.data || []) as any;
|
||||
if (myClassList.value.length > 0) {
|
||||
activeClassId.value = String(myClassList.value[0].id);
|
||||
await loadDormTree();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取班主任宿舍楼树 */
|
||||
const loadDormTree = async () => {
|
||||
try {
|
||||
const res = await getClassTeacherDormTree(activeClassId.value);
|
||||
dormOptions.value = (res.data || []).map((node: any) => ({
|
||||
...node,
|
||||
disabled: node.type !== 'building'
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('获取宿舍楼树失败', e);
|
||||
}
|
||||
};
|
||||
|
||||
/** 节点点击:仅 building 有效 */
|
||||
const handleNodeClick = (data: any) => {
|
||||
if (data.type !== 'building') return;
|
||||
selectedBuilding.value = data;
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 查询(重新加载房间床位数据) */
|
||||
const handleQuery = () => {
|
||||
loadRoomData();
|
||||
};
|
||||
|
||||
/** 重置查询 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields?.();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 加载房间与床位(使用新的班级房间查询接口) */
|
||||
const loadRoomData = async () => {
|
||||
if (!selectedBuilding.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
// 1. 调用新的班级房间查询接口,获取指定班级在指定宿舍楼的房间信息
|
||||
const roomRes = await listDormRoomByClassId(activeClassId.value as any, {
|
||||
buildingId: selectedBuilding.value.id as any,
|
||||
pageNum: 1,
|
||||
pageSize: 1000 // 设置较大的页面大小获取所有数据
|
||||
});
|
||||
|
||||
// 后端返回的是TableDataInfo格式,从rows字段获取实际数据
|
||||
const roomData = (roomRes.data as any)?.rows || roomRes.data || [];
|
||||
|
||||
// 2. 为每个房间获取床位详细信息
|
||||
const roomsWithBeds = await Promise.all(
|
||||
roomData.map(async (room: any) => {
|
||||
try {
|
||||
const bedRes = await getDormBedDetailsByRoomId(room.id);
|
||||
const bedData = bedRes.data || [];
|
||||
return {
|
||||
...room,
|
||||
isBed: false,
|
||||
children: bedData.map((bed: any) => ({
|
||||
...bed,
|
||||
isBed: true
|
||||
}))
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn(`获取房间 ${room.id} 的床位信息失败:`, e);
|
||||
return {
|
||||
...room,
|
||||
isBed: false,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
roomList.value = roomsWithBeds;
|
||||
// 默认清空选择
|
||||
nextTick(() => tableRef.value?.clearSelection());
|
||||
selectedRows.value = [];
|
||||
updateSelectionCount([]);
|
||||
} catch (e) {
|
||||
console.error('加载房间床位失败', e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 过滤后的房间数据(前端过滤) */
|
||||
const filteredRoomList = computed(() => {
|
||||
const name = (queryParams.roomName || '').trim().toLowerCase();
|
||||
const status = queryParams.roomStatus;
|
||||
if (!name && !status) return roomList.value;
|
||||
const matchRoom = (room: any) => {
|
||||
const nameOk = !name || (room.roomName || '').toLowerCase().includes(name);
|
||||
const statusOk = !status || room.roomStatus === status;
|
||||
return nameOk && statusOk;
|
||||
};
|
||||
return roomList.value
|
||||
.filter((room) => matchRoom(room))
|
||||
.map((room) => ({
|
||||
...room,
|
||||
children: room.children || []
|
||||
}));
|
||||
});
|
||||
|
||||
/** 展开/收起 */
|
||||
const handleExpandChange = (row: any, expanded: boolean) => {
|
||||
const key = String(row.id);
|
||||
if (expanded) expandedRowKeys.value.push(key);
|
||||
else expandedRowKeys.value = expandedRowKeys.value.filter((k) => k !== key);
|
||||
};
|
||||
const expandAll = async () => {
|
||||
if (expandAllLoading.value || !filteredRoomList.value?.length) return;
|
||||
expandAllLoading.value = true;
|
||||
await nextTick();
|
||||
expandedRowKeys.value = filteredRoomList.value.map((room) => String(room.id));
|
||||
await nextTick();
|
||||
expandAllLoading.value = false;
|
||||
};
|
||||
const collapseAll = async () => {
|
||||
if (collapseAllLoading.value) return;
|
||||
collapseAllLoading.value = true;
|
||||
await nextTick();
|
||||
expandedRowKeys.value = [];
|
||||
await nextTick();
|
||||
collapseAllLoading.value = false;
|
||||
};
|
||||
|
||||
/** 行可选(仅未分配且非维修中的床位可选) */
|
||||
const checkSelectable = (row: any) => {
|
||||
return row.isBed && !row.studentId && row.bedStatus !== '2';
|
||||
};
|
||||
|
||||
/** 选择变化 */
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectedRows.value = rows;
|
||||
updateSelectionCount(rows);
|
||||
};
|
||||
|
||||
/** 更新选择统计 */
|
||||
const updateSelectionCount = (rows: any[]) => {
|
||||
const bedStats = { available: 0, allocated: 0, maintenance: 0 } as any;
|
||||
rows.forEach((row) => {
|
||||
if (!row.isBed) return;
|
||||
if (row.bedStatus === '2') bedStats.maintenance++;
|
||||
else if (row.studentId) bedStats.allocated++;
|
||||
else bedStats.available++;
|
||||
});
|
||||
selectedBedCount.value = rows.filter((r) => r.isBed).length;
|
||||
selectedBedStats.value = bedStats;
|
||||
};
|
||||
|
||||
/** 打开选择性分配弹窗 */
|
||||
const selectiveDialog = reactive({ visible: false, title: '分配床位给学生' });
|
||||
const openSelectiveAllocation = () => {
|
||||
if (!selectedBuilding.value) {
|
||||
ElMessage.warning('请先选择宿舍楼');
|
||||
return;
|
||||
}
|
||||
selectiveDialog.visible = true;
|
||||
};
|
||||
|
||||
/** 分配成功回调(刷新) */
|
||||
const handleAllocationSuccess = () => {
|
||||
loadRoomData();
|
||||
};
|
||||
|
||||
/** 一键随机分配 */
|
||||
const handleRandomAllocation = async () => {
|
||||
if (!selectedBuilding.value) return ElMessage.warning('请先选择宿舍楼');
|
||||
try {
|
||||
await ElMessageBox.confirm('确认对本班级在该楼栋内进行随机分配吗?', '提示', { type: 'warning' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
actionLoading.value = true;
|
||||
await classTeacherRandomAllocation(selectedBuilding.value.id as any, activeClassId.value as any);
|
||||
ElMessage.success('随机分配成功');
|
||||
loadRoomData();
|
||||
} catch (e) {
|
||||
ElMessage.error('随机分配失败');
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 按学号分配 */
|
||||
const handleStudentNoAllocation = async () => {
|
||||
if (!selectedBuilding.value) return ElMessage.warning('请先选择宿舍楼');
|
||||
try {
|
||||
await ElMessageBox.confirm('确认按学号顺序为学生分配床位吗?', '提示', { type: 'warning' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
actionLoading.value = true;
|
||||
await classTeacherStudentNoAllocation(selectedBuilding.value.id as any, activeClassId.value as any);
|
||||
ElMessage.success('按学号分配成功');
|
||||
loadRoomData();
|
||||
} catch (e) {
|
||||
ElMessage.error('按学号分配失败');
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 一键收回 */
|
||||
const handleRecall = async () => {
|
||||
if (!selectedBuilding.value) return ElMessage.warning('请先选择宿舍楼');
|
||||
try {
|
||||
await ElMessageBox.confirm('确认收回本班级在该楼栋内的所有床位分配吗?', '提示', { type: 'warning' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
actionLoading.value = true;
|
||||
await classTeacherRecallBeds(selectedBuilding.value.id as any, activeClassId.value as any);
|
||||
ElMessage.success('收回成功');
|
||||
loadRoomData();
|
||||
} catch (e) {
|
||||
ElMessage.error('收回失败');
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadMyClassList();
|
||||
});
|
||||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
@@ -95,9 +95,9 @@
|
||||
<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="primary" icon="UserFilled" size="small" @click="handleSelectiveAllocation">分配床位</el-button>
|
||||
<!-- <el-button type="success" icon="UserFilled" 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">
|
||||
@@ -136,6 +136,16 @@
|
||||
- 床位分配管理</span
|
||||
>
|
||||
<div class="text-blue-500 flex flex-wrap items-center gap-2">
|
||||
<span v-if="unallocatedStudentStats.total > 0" class="text-orange-500 text-sm">
|
||||
未分配学生:
|
||||
<el-tag size="small" type="warning">{{ unallocatedStudentStats.total }}</el-tag
|
||||
>人
|
||||
<span class="ml-2 text-xs">
|
||||
[男生 <el-tag size="small" type="primary">{{ unallocatedStudentStats.male }}</el-tag
|
||||
>人, 女生 <el-tag size="small" class="female-tag">{{ unallocatedStudentStats.female }}</el-tag
|
||||
>人]
|
||||
</span>
|
||||
</span>
|
||||
<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
|
||||
@@ -143,10 +153,6 @@
|
||||
>个, 维修中: <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>
|
||||
@@ -170,10 +176,16 @@
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="160">
|
||||
<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>
|
||||
<span v-if="!scope.row.isBed" class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><House /></el-icon>
|
||||
{{ scope.row.roomName }}
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
{{ scope.row.bedName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="floorNum" label="楼层" width="80" align="center">
|
||||
@@ -277,6 +289,7 @@
|
||||
:title="selectiveAllocationDialog.title"
|
||||
:building-info="selectiveAllocationDialog.buildingInfo"
|
||||
:class-id="activeClassId"
|
||||
:parent-class-info="currentClassInfo"
|
||||
@success="handleAllocationSuccess"
|
||||
/>
|
||||
</div>
|
||||
@@ -284,16 +297,18 @@
|
||||
|
||||
<script setup name="ClassTeacherBedAllocation" lang="ts">
|
||||
import { ref, computed, reactive, onMounted, nextTick, watch } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { InfoFilled, House, Avatar } 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';
|
||||
// 导入API方法
|
||||
import { listDormRoomByClassId } from '@/api/xg/xgss/dormRoom/index';
|
||||
import { getBedDetailsByRoomIdAndClassId } from '@/api/xg/xgss/dormBed/index';
|
||||
import { getMyClassListAsTeacher } from '@/api/xg/xsjcxx/classMsg/classMsg';
|
||||
// 定义一个最小班级信息类型以避免跨文件类型不兼容
|
||||
interface SimpleClassVO {
|
||||
@@ -329,6 +344,11 @@ const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve,
|
||||
const classList = ref<SimpleClassVO[]>([]);
|
||||
const activeClassId = ref<string>('');
|
||||
|
||||
// 当前选中的班级信息
|
||||
const currentClassInfo = computed(() => {
|
||||
return classList.value.find((cls) => String(cls.id) === activeClassId.value) || null;
|
||||
});
|
||||
|
||||
// 选择状态
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const selectedRoomCount = ref(0);
|
||||
@@ -342,6 +362,12 @@ const selectedBedStats = ref({
|
||||
|
||||
// 未分配学生数量
|
||||
const unallocatedStudentCount = ref(0);
|
||||
// 按性别统计的未分配学生数量
|
||||
const unallocatedStudentStats = ref({
|
||||
male: 0, // 男生数量
|
||||
female: 0, // 女生数量
|
||||
total: 0 // 总数量
|
||||
});
|
||||
|
||||
const animate = reactive({
|
||||
searchAnimate: {
|
||||
@@ -432,6 +458,7 @@ const initForActiveClass = async () => {
|
||||
selectedBedCount.value = 0;
|
||||
selectedBedStats.value = { available: 0, allocated: 0, maintenance: 0 };
|
||||
unallocatedStudentCount.value = 0;
|
||||
unallocatedStudentStats.value = { male: 0, female: 0, total: 0 };
|
||||
// 重新加载宿舍树
|
||||
await getDormTreeData();
|
||||
};
|
||||
@@ -485,18 +512,72 @@ const resetQuery = () => {
|
||||
|
||||
/** 加载房间数据 */
|
||||
const loadRoomData = async () => {
|
||||
if (!queryParams.buildingId) return;
|
||||
if (!queryParams.buildingId || !activeClassId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
const controller = currentController;
|
||||
|
||||
try {
|
||||
// 获取班主任管理班级的房间床位详情
|
||||
const res = await getClassTeacherBedDetails(queryParams.buildingId, activeClassId.value);
|
||||
// 构建查询参数
|
||||
const query = {
|
||||
buildingId: queryParams.buildingId,
|
||||
roomName: queryParams.roomName,
|
||||
roomStatus: queryParams.roomStatus,
|
||||
pageNum: queryParams.pageNum,
|
||||
pageSize: queryParams.pageSize
|
||||
};
|
||||
|
||||
// 获取指定班级的宿舍房间信息列表
|
||||
const res = await listDormRoomByClassId(activeClassId.value, query);
|
||||
const rooms = res.rows || [];
|
||||
|
||||
if (controller?.signal?.aborted) return;
|
||||
|
||||
roomList.value = res.data || [];
|
||||
// 并行获取所有房间的床位详细信息
|
||||
const roomPromises = rooms.map(async (room) => {
|
||||
// 创建房间记录
|
||||
const roomRecord = {
|
||||
...room,
|
||||
isBed: false,
|
||||
children: []
|
||||
};
|
||||
|
||||
try {
|
||||
// 调用新的接口获取指定房间和班级的床位详细信息
|
||||
const bedRes = await getBedDetailsByRoomIdAndClassId(room.id, activeClassId.value);
|
||||
|
||||
// 如果请求已被取消,不继续处理
|
||||
if (controller?.signal?.aborted) return roomRecord;
|
||||
|
||||
const beds = bedRes.data || [];
|
||||
|
||||
// 将床位信息转换为符合表格展示的格式
|
||||
if (beds.length > 0) {
|
||||
roomRecord.children = beds.map((bed) => ({
|
||||
...bed,
|
||||
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;
|
||||
|
||||
console.log('房间数据:', tempList);
|
||||
|
||||
// 构建床位到房间的映射缓存
|
||||
buildBedToRoomMap();
|
||||
@@ -520,6 +601,12 @@ const loadRoomData = async () => {
|
||||
isUpdatingSelection.value = false;
|
||||
}
|
||||
|
||||
// 自动展开所有房间行,这样用户可以立即看到床位信息
|
||||
await nextTick();
|
||||
await nextTick(); // 双重 nextTick 确保 DOM 完全更新
|
||||
expandedRowKeys.value = tempList.map((room) => String(room.id));
|
||||
console.log('设置展开的房间ID:', expandedRowKeys.value);
|
||||
|
||||
// 获取未分配学生数量
|
||||
await getUnallocatedStudentCount();
|
||||
} catch (error: any) {
|
||||
@@ -537,7 +624,22 @@ const loadRoomData = async () => {
|
||||
const getUnallocatedStudentCount = async () => {
|
||||
try {
|
||||
const res = await getClassTeacherUnallocatedStudents(activeClassId.value);
|
||||
unallocatedStudentCount.value = res.data?.length || 0;
|
||||
const students = res.data || [];
|
||||
|
||||
// 按性别统计
|
||||
const maleCount = students.filter((student) => student.gender === '0').length;
|
||||
const femaleCount = students.filter((student) => student.gender === '1').length;
|
||||
const totalCount = students.length;
|
||||
|
||||
// 更新统计数据
|
||||
unallocatedStudentStats.value = {
|
||||
male: maleCount,
|
||||
female: femaleCount,
|
||||
total: totalCount
|
||||
};
|
||||
|
||||
// 保持向后兼容
|
||||
unallocatedStudentCount.value = totalCount;
|
||||
} catch (error) {
|
||||
console.error('获取未分配学生数量失败', error);
|
||||
}
|
||||
@@ -638,8 +740,15 @@ const handleRecallBeds = async () => {
|
||||
const handleAllocationSuccess = () => {
|
||||
// 清除缓存,确保数据刷新
|
||||
selectableRowsCache.value = [];
|
||||
// 保存当前展开状态
|
||||
const currentExpandedKeys = [...expandedRowKeys.value];
|
||||
// 刷新房间和床位数据
|
||||
loadRoomData();
|
||||
loadRoomData().then(() => {
|
||||
// 数据刷新后,恢复展开状态
|
||||
nextTick(() => {
|
||||
expandedRowKeys.value = currentExpandedKeys;
|
||||
});
|
||||
});
|
||||
// 清空选择状态
|
||||
isUpdatingSelection.value = true;
|
||||
try {
|
||||
@@ -647,6 +756,7 @@ const handleAllocationSuccess = () => {
|
||||
selectedRoomCount.value = 0;
|
||||
selectedBedCount.value = 0;
|
||||
selectedBedStats.value = { available: 0, allocated: 0, maintenance: 0 };
|
||||
unallocatedStudentStats.value = { male: 0, female: 0, total: 0 };
|
||||
} finally {
|
||||
isUpdatingSelection.value = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="title" width="1200px" :before-close="handleClose" destroy-on-close class="selective-allocation-dialog">
|
||||
<div class="allocation-container">
|
||||
<!-- 左侧:床位选择区域 -->
|
||||
<div class="selection-area">
|
||||
<div class="area-header">
|
||||
<h3 class="area-title">
|
||||
<el-icon><House /></el-icon>
|
||||
可用床位选择
|
||||
</h3>
|
||||
<div class="stats">
|
||||
已选择 <el-tag type="primary" size="small">{{ selectedBeds.length }}</el-tag> 个床位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input v-model="bedSearchKey" placeholder="搜索房间号、床位号" prefix-icon="Search" clearable @input="handleBedSearch" />
|
||||
</div>
|
||||
|
||||
<div class="bed-list" v-loading="bedLoading">
|
||||
<el-table
|
||||
ref="bedTableRef"
|
||||
:data="filteredBedList"
|
||||
row-key="id"
|
||||
height="400"
|
||||
@selection-change="handleBedSelectionChange"
|
||||
@select-all="handleBedSelectAll"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkBedSelectable" />
|
||||
<el-table-column prop="roomName" label="房间" width="100" />
|
||||
<el-table-column prop="bedName" label="床位" width="100" />
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.bedType === '1'" type="success" size="small">下铺</el-tag>
|
||||
<el-tag v-else-if="scope.row.bedType === '2'" type="warning" size="small">上铺</el-tag>
|
||||
<el-tag v-else-if="scope.row.bedType === '3'" type="danger" size="small">中铺</el-tag>
|
||||
<el-tag v-else type="info" size="small">独立床</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default>
|
||||
<el-tag type="success" size="small">空闲</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:操作区域 -->
|
||||
<div class="operation-area">
|
||||
<div class="match-info">
|
||||
<el-icon class="match-icon"><Connection /></el-icon>
|
||||
<div class="match-stats">
|
||||
<div class="stat-item">
|
||||
<span class="label">床位:</span>
|
||||
<span class="value">{{ selectedBeds.length }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">学生:</span>
|
||||
<span class="value">{{ selectedStudents.length }}</span>
|
||||
</div>
|
||||
<div class="match-status" :class="{ 'matched': isMatched, 'unmatched': !isMatched }">
|
||||
{{ isMatched ? '✓ 数量匹配' : '✗ 数量不匹配' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="allocation-buttons">
|
||||
<el-button type="success" icon="Refresh" :disabled="!isMatched" @click="handleRandomAllocation"> 随机分配 </el-button>
|
||||
<el-button type="primary" icon="Sort" :disabled="!isMatched" @click="handleStudentNoAllocation"> 按学号分配 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:学生选择区域 -->
|
||||
<div class="selection-area">
|
||||
<div class="area-header">
|
||||
<h3 class="area-title">
|
||||
<el-icon><User /></el-icon>
|
||||
未分配学生选择
|
||||
</h3>
|
||||
<div class="stats">
|
||||
已选择 <el-tag type="warning" size="small">{{ selectedStudents.length }}</el-tag> 名学生
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input v-model="studentSearchKey" placeholder="搜索学号、姓名" prefix-icon="Search" clearable @input="handleStudentSearch" />
|
||||
</div>
|
||||
|
||||
<div class="student-list" v-loading="studentLoading">
|
||||
<el-table
|
||||
ref="studentTableRef"
|
||||
:data="filteredStudentList"
|
||||
row-key="id"
|
||||
height="400"
|
||||
@selection-change="handleStudentSelectionChange"
|
||||
@select-all="handleStudentSelectAll"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="studentNo" label="学号" width="120" />
|
||||
<el-table-column prop="studentName" label="姓名" width="100" />
|
||||
<el-table-column label="性别" width="60" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.studentGender === '0'" type="primary" size="small">男</el-tag>
|
||||
<el-tag v-else-if="scope.row.studentGender === '1'" class="female-tag" size="small">女</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="className" label="班级" min-width="120" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!isMatched">确认分配</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { House, User, Connection } from '@element-plus/icons-vue';
|
||||
import type { DormBuildingTreeVO } from '@/api/xg/xgss/dormBedStudent/dormBedStudent/types';
|
||||
import {
|
||||
getClassTeacherBedDetails,
|
||||
getClassTeacherUnallocatedStudents,
|
||||
classTeacherSelectiveAllocation
|
||||
} from '@/api/xg/xgss/dormBedStudent/dormBedStudent';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
|
||||
// 定义组件选项
|
||||
defineOptions({
|
||||
name: 'SelectiveAllocationDialog'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
title?: string;
|
||||
buildingInfo?: DormBuildingTreeVO;
|
||||
classId?: string | number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'success'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '选择性分配床位'
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 对话框显示状态
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
});
|
||||
|
||||
// 加载状态
|
||||
const bedLoading = ref(false);
|
||||
const studentLoading = ref(false);
|
||||
|
||||
// 搜索关键字
|
||||
const bedSearchKey = ref('');
|
||||
const studentSearchKey = ref('');
|
||||
|
||||
// 表格引用
|
||||
const bedTableRef = ref();
|
||||
const studentTableRef = ref();
|
||||
|
||||
// 数据列表
|
||||
const bedList = ref<any[]>([]);
|
||||
const studentList = ref<any[]>([]);
|
||||
const selectedBeds = ref<any[]>([]);
|
||||
const selectedStudents = ref<any[]>([]);
|
||||
|
||||
// 计算属性:过滤后的列表
|
||||
const filteredBedList = computed(() => {
|
||||
if (!bedSearchKey.value) return bedList.value;
|
||||
const key = bedSearchKey.value.toLowerCase();
|
||||
return bedList.value.filter((bed) => (bed.roomName || '').toLowerCase().includes(key) || (bed.bedName || '').toLowerCase().includes(key));
|
||||
});
|
||||
|
||||
const filteredStudentList = computed(() => {
|
||||
if (!studentSearchKey.value) return studentList.value;
|
||||
const key = studentSearchKey.value.toLowerCase();
|
||||
return studentList.value.filter(
|
||||
(student) => (student.studentNo || '').toLowerCase().includes(key) || (student.studentName || '').toLowerCase().includes(key)
|
||||
);
|
||||
});
|
||||
|
||||
// 计算属性:是否匹配
|
||||
const isMatched = computed(() => {
|
||||
return selectedBeds.value.length > 0 && selectedStudents.value.length > 0 && selectedBeds.value.length === selectedStudents.value.length;
|
||||
});
|
||||
|
||||
// 监听建筑信息变化,加载数据
|
||||
watch(
|
||||
() => props.buildingInfo,
|
||||
async (newValue) => {
|
||||
if (newValue && props.modelValue) {
|
||||
await loadData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听对话框打开,加载数据
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (newValue) => {
|
||||
if (newValue && props.buildingInfo) {
|
||||
await loadData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/** 加载数据 */
|
||||
const loadData = async () => {
|
||||
await Promise.all([loadBedData(), loadStudentData()]);
|
||||
};
|
||||
|
||||
/** 加载床位数据 */
|
||||
const loadBedData = async () => {
|
||||
if (!props.buildingInfo) return;
|
||||
|
||||
bedLoading.value = true;
|
||||
try {
|
||||
const res = await getClassTeacherBedDetails(props.buildingInfo.id as any, props.classId as any);
|
||||
const allBeds: any[] = [];
|
||||
(res.data || []).forEach((room: any) => {
|
||||
if (room.children && room.children.length > 0) {
|
||||
room.children.forEach((bed: any) => {
|
||||
// 只要空闲床位(没有学生分配)
|
||||
if (!bed.studentId && bed.bedStatus !== '2') {
|
||||
allBeds.push({
|
||||
...bed,
|
||||
roomName: room.roomName
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
bedList.value = allBeds;
|
||||
} catch (error) {
|
||||
console.error('获取床位数据失败', error);
|
||||
ElMessage.error('获取床位数据失败');
|
||||
} finally {
|
||||
bedLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 加载学生数据 */
|
||||
const loadStudentData = async () => {
|
||||
studentLoading.value = true;
|
||||
try {
|
||||
const res = await getClassTeacherUnallocatedStudents(props.classId as any);
|
||||
studentList.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取学生数据失败', error);
|
||||
ElMessage.error('获取学生数据失败');
|
||||
} finally {
|
||||
studentLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 床位搜索 */
|
||||
const handleBedSearch = () => {};
|
||||
/** 学生搜索 */
|
||||
const handleStudentSearch = () => {};
|
||||
|
||||
/** 检查床位是否可选 */
|
||||
const checkBedSelectable = (row: any) => {
|
||||
return !row.studentId && row.bedStatus !== '2';
|
||||
};
|
||||
|
||||
/** 床位选择变化 */
|
||||
const handleBedSelectionChange = (selection: any[]) => {
|
||||
selectedBeds.value = selection;
|
||||
};
|
||||
/** 床位全选 */
|
||||
const handleBedSelectAll = (selection: any[]) => {
|
||||
selectedBeds.value = selection;
|
||||
};
|
||||
|
||||
/** 学生选择变化 */
|
||||
const handleStudentSelectionChange = (selection: any[]) => {
|
||||
selectedStudents.value = selection;
|
||||
};
|
||||
/** 学生全选 */
|
||||
const handleStudentSelectAll = (selection: any[]) => {
|
||||
selectedStudents.value = selection;
|
||||
};
|
||||
|
||||
/** 随机分配 */
|
||||
const handleRandomAllocation = async () => {
|
||||
if (!isMatched.value) {
|
||||
ElMessage.warning('请确保床位和学生数量匹配');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const allocationParams = {
|
||||
bedIds: selectedBeds.value.map((bed) => bed.id),
|
||||
studentIds: selectedStudents.value.map((student) => student.id),
|
||||
allocationType: 'random' as const
|
||||
};
|
||||
|
||||
await classTeacherSelectiveAllocation(allocationParams, props.classId as any);
|
||||
ElMessage.success('随机分配成功');
|
||||
handleSuccess();
|
||||
} catch (error) {
|
||||
ElMessage.error('随机分配失败');
|
||||
}
|
||||
};
|
||||
|
||||
/** 按学号分配 */
|
||||
const handleStudentNoAllocation = async () => {
|
||||
if (!isMatched.value) {
|
||||
ElMessage.warning('请确保床位和学生数量匹配');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const allocationParams = {
|
||||
bedIds: selectedBeds.value.map((bed) => bed.id),
|
||||
studentIds: selectedStudents.value.map((student) => student.id),
|
||||
allocationType: 'studentNo' as const
|
||||
};
|
||||
|
||||
await classTeacherSelectiveAllocation(allocationParams, props.classId as any);
|
||||
ElMessage.success('按学号分配成功');
|
||||
handleSuccess();
|
||||
} catch (error) {
|
||||
ElMessage.error('按学号分配失败');
|
||||
}
|
||||
};
|
||||
|
||||
/** 确认分配(默认随机) */
|
||||
const handleConfirm = () => {
|
||||
handleRandomAllocation();
|
||||
};
|
||||
|
||||
/** 分配成功处理 */
|
||||
const handleSuccess = () => {
|
||||
emit('success');
|
||||
handleClose();
|
||||
};
|
||||
|
||||
/** 关闭对话框 */
|
||||
const handleClose = () => {
|
||||
selectedBeds.value = [];
|
||||
selectedStudents.value = [];
|
||||
bedSearchKey.value = '';
|
||||
studentSearchKey.value = '';
|
||||
bedList.value = [];
|
||||
studentList.value = [];
|
||||
|
||||
nextTick(() => {
|
||||
bedTableRef.value?.clearSelection();
|
||||
studentTableRef.value?.clearSelection();
|
||||
});
|
||||
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selective-allocation-dialog :deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.allocation-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.selection-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.area-header {
|
||||
background: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.area-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.bed-list,
|
||||
.student-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.operation-area {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.match-info {
|
||||
text-align: center;
|
||||
}
|
||||
.match-icon {
|
||||
font-size: 32px;
|
||||
color: #909399;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.match-stats {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.label {
|
||||
color: #606266;
|
||||
}
|
||||
.value {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.match-status {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.match-status.matched {
|
||||
background: #f0f9ff;
|
||||
color: #67c23a;
|
||||
border: 1px solid #b3e19d;
|
||||
}
|
||||
.match-status.unmatched {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.allocation-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.allocation-buttons .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.female-tag {
|
||||
background-color: rgba(255, 105, 180, 0.1) !important;
|
||||
color: #ff69b4 !important;
|
||||
border-color: rgba(255, 105, 180, 0.1) !important;
|
||||
}
|
||||
|
||||
.selection-area :deep(.el-table) {
|
||||
border: none;
|
||||
}
|
||||
.selection-area :deep(.el-table__header) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.selection-area :deep(.el-table th) {
|
||||
background: #f8f9fa !important;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
.selection-area :deep(.el-table td) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.selection-area :deep(.el-table__row:hover > td) {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -55,7 +55,7 @@
|
||||
|
||||
<!-- 学生树 -->
|
||||
<el-card shadow="hover" class="tree-card mt-4">
|
||||
<div class="card-header-title">学生信息</div>
|
||||
<div class="card-header-title">班级床位需求信息(已分配/总需求)</div>
|
||||
<el-input
|
||||
v-model="studentSearchKey"
|
||||
prefix-icon="Search"
|
||||
@@ -205,10 +205,16 @@
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="160">
|
||||
<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>
|
||||
<span v-if="!scope.row.isBed" class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><House /></el-icon>
|
||||
{{ scope.row.roomName }}
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
{{ scope.row.bedName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="floorNum" label="楼层" width="80" align="center">
|
||||
@@ -352,7 +358,7 @@
|
||||
|
||||
<script setup name="DormRoomTreeManagement" lang="ts">
|
||||
import { ref, computed, reactive, onMounted, nextTick, watch, getCurrentInstance } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { InfoFilled, House, Avatar } from '@element-plus/icons-vue';
|
||||
import type { DormBuildingTreeVO } from '@/api/xg/xgss/dormBedStudent/dormBedStudent/types';
|
||||
import type { DormTreeNode, BedInfo } from '@/api/xg/xgss/xsssfjgl/types';
|
||||
import type { DormRoomVO } from '@/api/xg/xgss/dormRoom/types';
|
||||
|
||||
@@ -205,10 +205,16 @@
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkSelectable" />
|
||||
<el-table-column prop="roomName" label="房间/宿舍名称" min-width="160">
|
||||
<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>
|
||||
<span v-if="!scope.row.isBed" class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><House /></el-icon>
|
||||
{{ scope.row.roomName }}
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
{{ scope.row.bedName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="floorNum" label="楼层" width="80" align="center">
|
||||
@@ -350,7 +356,7 @@
|
||||
|
||||
<script setup name="DormRoomTreeManagement" lang="ts">
|
||||
import { ref, computed, reactive, onMounted, nextTick, watch, getCurrentInstance } from 'vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { InfoFilled, House, Avatar } 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';
|
||||
|
||||
Reference in new issue
Block a user