|
|
@@ -118,6 +118,57 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-tree>
|
|
|
+
|
|
|
+ <!-- 学院人数分配 -->
|
|
|
+ <div v-if="selectedCollegeIds.length > 0" class="college-allocation mt-4">
|
|
|
+ <div class="allocation-header mb-3">
|
|
|
+ <span class="font-bold text-gray-700">学院床位分配</span>
|
|
|
+ <span class="text-gray-500 text-sm ml-2">(为每个学院指定分配的床位数)</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="allocation-form">
|
|
|
+ <el-form :model="allocationForm" label-width="120px" size="small">
|
|
|
+ <el-form-item
|
|
|
+ v-for="collegeId in selectedCollegeIds"
|
|
|
+ :key="collegeId"
|
|
|
+ :label="getCollegeName(collegeId)"
|
|
|
+ :rules="[
|
|
|
+ { required: true, message: '请输入床位数', trigger: 'blur' },
|
|
|
+ {
|
|
|
+ type: 'number',
|
|
|
+ min: 0,
|
|
|
+ max: props.selectedBeds.length,
|
|
|
+ message: `床位数必须在1-${props.selectedBeds.length}之间`,
|
|
|
+ trigger: 'blur'
|
|
|
+ }
|
|
|
+ ]"
|
|
|
+ >
|
|
|
+ <el-input-number
|
|
|
+ v-model="allocationForm[collegeId]"
|
|
|
+ :min="0"
|
|
|
+ :max="props.selectedBeds.length"
|
|
|
+ :precision="0"
|
|
|
+ placeholder="请输入床位数"
|
|
|
+ style="width: 200px"
|
|
|
+ @change="handleAllocationChange"
|
|
|
+ />
|
|
|
+ <span class="text-gray-500 ml-2">个床位</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 分配统计信息 -->
|
|
|
+ <div class="allocation-stats mt-3">
|
|
|
+ <el-alert
|
|
|
+ :title="getAllocationStatsTitle()"
|
|
|
+ :description="getAllocationStatsDescription()"
|
|
|
+ :type="getAllocationStatsType()"
|
|
|
+ show-icon
|
|
|
+ :closable="false"
|
|
|
+ class="allocation-alert"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</el-card>
|
|
|
</div>
|
|
|
|
|
|
@@ -168,6 +219,9 @@ const selectedCollegeIds = ref<(string | number)[]>([]);
|
|
|
const collegeTreeData = ref<XgStudentTreeVO[]>([]);
|
|
|
const bedTreeData = ref<any[]>([]);
|
|
|
|
|
|
+// 学院人数分配表单
|
|
|
+const allocationForm = ref<Record<string | number, number>>({});
|
|
|
+
|
|
|
// 计算属性
|
|
|
const dialogVisible = computed({
|
|
|
get: () => props.modelValue,
|
|
|
@@ -180,6 +234,16 @@ const roomCount = computed(() => {
|
|
|
return roomNames.size;
|
|
|
});
|
|
|
|
|
|
+// 计算已分配的床位数
|
|
|
+const allocatedBedsCount = computed(() => {
|
|
|
+ return Object.values(allocationForm.value).reduce((sum, count) => sum + (count || 0), 0);
|
|
|
+});
|
|
|
+
|
|
|
+// 计算剩余床位数
|
|
|
+const remainingBedsCount = computed(() => {
|
|
|
+ return props.selectedBeds.length - allocatedBedsCount.value;
|
|
|
+});
|
|
|
+
|
|
|
// 构建床位树形数据
|
|
|
const buildBedTreeData = () => {
|
|
|
if (!props.selectedBeds || props.selectedBeds.length === 0) {
|
|
|
@@ -317,12 +381,98 @@ const handleCheckChange = (data: XgStudentTreeVO, checked: boolean) => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 检查是否还有剩余床位可分配
|
|
|
+ if (checked && props.selectedBeds.length === 0) {
|
|
|
+ // 如果没有床位可分配,阻止勾选并给出提示
|
|
|
+ nextTick(() => {
|
|
|
+ collegeTreeRef.value?.setChecked(data.id, false, false);
|
|
|
+ });
|
|
|
+ ElMessage.warning('当前没有床位可分配,请先选择床位');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// 更新选中的学院ID列表
|
|
|
const checkedKeys = collegeTreeRef.value?.getCheckedKeys() || [];
|
|
|
selectedCollegeIds.value = checkedKeys.filter((key: any) => {
|
|
|
const node = collegeTreeRef.value?.getNode(key);
|
|
|
return node?.data?.type === 'college';
|
|
|
});
|
|
|
+
|
|
|
+ // 初始化人数分配表单
|
|
|
+ initAllocationForm();
|
|
|
+};
|
|
|
+
|
|
|
+// 初始化人数分配表单
|
|
|
+const initAllocationForm = () => {
|
|
|
+ allocationForm.value = {};
|
|
|
+
|
|
|
+ if (selectedCollegeIds.value.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否还有床位可分配
|
|
|
+ if (props.selectedBeds.length === 0) {
|
|
|
+ // 如果没有床位可分配,清空选中的学院并给出提示
|
|
|
+ selectedCollegeIds.value = [];
|
|
|
+ nextTick(() => {
|
|
|
+ // 取消所有勾选状态
|
|
|
+ collegeTreeRef.value?.setCheckedKeys([]);
|
|
|
+ });
|
|
|
+ ElMessage.warning('当前没有床位可分配,请先选择床位');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新的床位分配规则:尽量满足学院需求
|
|
|
+ // 1. 按学院顺序逐个分配
|
|
|
+ // 2. 如果学院需求床位数 <= 剩余床位数,则满足学院需求
|
|
|
+ // 3. 如果学院需求床位数 > 剩余床位数,则分配所有剩余床位
|
|
|
+
|
|
|
+ const totalBeds = props.selectedBeds.length;
|
|
|
+ let remainingBeds = totalBeds;
|
|
|
+
|
|
|
+ // 获取学院的真实床位需求数(总需求数 - 已分配数)
|
|
|
+ const getCollegeDemand = (collegeId: string | number) => {
|
|
|
+ // 从学院树数据中查找学院节点
|
|
|
+ const college = findCollegeById(collegeTreeData.value, collegeId);
|
|
|
+ if (!college) return 0;
|
|
|
+
|
|
|
+ // 根据宿舍楼类型确定性别
|
|
|
+ if (props.buildingInfo?.buildingType === '1') {
|
|
|
+ // 男生楼,计算男生床位需求
|
|
|
+ const maleDemand = (college.maleTotal || 0) - (college.maleAssigned || 0);
|
|
|
+ return Math.max(0, maleDemand); // 确保不为负数
|
|
|
+ } else if (props.buildingInfo?.buildingType === '2') {
|
|
|
+ // 女生楼,计算女生床位需求
|
|
|
+ const femaleDemand = (college.femaleTotal || 0) - (college.femaleAssigned || 0);
|
|
|
+ return Math.max(0, femaleDemand); // 确保不为负数
|
|
|
+ } else {
|
|
|
+ // 混合楼,计算总床位需求
|
|
|
+ const totalDemand = (college.maleTotal || 0) + (college.femaleTotal || 0) - (college.maleAssigned || 0) - (college.femaleAssigned || 0);
|
|
|
+ return Math.max(0, totalDemand); // 确保不为负数
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 按顺序为每个学院分配床位
|
|
|
+ selectedCollegeIds.value.forEach((collegeId, index) => {
|
|
|
+ let bedCount = 0;
|
|
|
+
|
|
|
+ if (remainingBeds > 0) {
|
|
|
+ // 获取学院的真实床位需求数
|
|
|
+ const collegeDemand = getCollegeDemand(collegeId);
|
|
|
+
|
|
|
+ if (remainingBeds >= collegeDemand) {
|
|
|
+ // 如果剩余床位数 >= 学院需求,则满足学院需求
|
|
|
+ bedCount = collegeDemand;
|
|
|
+ remainingBeds -= collegeDemand;
|
|
|
+ } else {
|
|
|
+ // 如果剩余床位数 < 学院需求,则分配所有剩余床位
|
|
|
+ bedCount = remainingBeds;
|
|
|
+ remainingBeds = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ allocationForm.value[collegeId] = bedCount;
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
// 获取学院树数据
|
|
|
@@ -357,6 +507,110 @@ const getCollegeTreeData = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+// 获取勾选统计信息标题
|
|
|
+const getSelectionStatsTitle = () => {
|
|
|
+ if (selectedCollegeIds.value.length === 0) {
|
|
|
+ return '未选择学院';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.buildingInfo?.buildingType === '1') {
|
|
|
+ return `已选择 ${selectedCollegeIds.value.length} 个学院(男生楼)`;
|
|
|
+ } else if (props.buildingInfo?.buildingType === '2') {
|
|
|
+ return `已选择 ${selectedCollegeIds.value.length} 个学院(女生楼)`;
|
|
|
+ }
|
|
|
+ return `已选择 ${selectedCollegeIds.value.length} 个学院`;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取勾选统计信息描述
|
|
|
+const getSelectionStatsDescription = () => {
|
|
|
+ if (selectedCollegeIds.value.length === 0) {
|
|
|
+ return '请勾选要分配的学院';
|
|
|
+ }
|
|
|
+ return `共选择了 ${selectedCollegeIds.value.length} 个学院,请为每个学院指定分配的床位数`;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取勾选统计信息类型
|
|
|
+const getSelectionStatsType = () => {
|
|
|
+ if (selectedCollegeIds.value.length === 0) {
|
|
|
+ return 'info';
|
|
|
+ }
|
|
|
+ return 'success';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取学院名称
|
|
|
+const getCollegeName = (collegeId: string | number) => {
|
|
|
+ // 直接从学院树数据中查找学院名称
|
|
|
+ const college = findCollegeById(collegeTreeData.value, collegeId);
|
|
|
+ if (college && college.label) {
|
|
|
+ return college.label;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果找不到,返回默认名称
|
|
|
+ return `学院${collegeId}`;
|
|
|
+};
|
|
|
+
|
|
|
+// 处理学院人数分配变化
|
|
|
+const handleAllocationChange = () => {
|
|
|
+ // 验证床位分配是否超出总床位数
|
|
|
+ const totalAllocated = Object.values(allocationForm.value).reduce((sum, count) => sum + (count || 0), 0);
|
|
|
+
|
|
|
+ if (totalAllocated > props.selectedBeds.length) {
|
|
|
+ ElMessage.warning(`床位分配超出:已分配 ${totalAllocated} 个,但只有 ${props.selectedBeds.length} 个床位可用`);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('学院人数分配变化:', allocationForm.value);
|
|
|
+ console.log('总分配床位数:', totalAllocated, '总可用床位数:', props.selectedBeds.length);
|
|
|
+};
|
|
|
+
|
|
|
+// 获取分配统计信息标题
|
|
|
+const getAllocationStatsTitle = () => {
|
|
|
+ if (remainingBedsCount.value === 0) {
|
|
|
+ return '床位分配完成';
|
|
|
+ } else if (remainingBedsCount.value > 0) {
|
|
|
+ return `床位分配进行中`;
|
|
|
+ } else {
|
|
|
+ return '床位分配超出';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取分配统计信息描述
|
|
|
+const getAllocationStatsDescription = () => {
|
|
|
+ if (remainingBedsCount.value === 0) {
|
|
|
+ return `已分配 ${allocatedBedsCount.value} 个床位,分配完成!`;
|
|
|
+ } else if (remainingBedsCount.value > 0) {
|
|
|
+ return `已分配 ${allocatedBedsCount.value} 个床位,还需分配 ${remainingBedsCount.value} 个床位`;
|
|
|
+ } else {
|
|
|
+ return `已分配 ${allocatedBedsCount.value} 个床位,超出 ${Math.abs(remainingBedsCount.value)} 个床位`;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取分配统计信息类型
|
|
|
+const getAllocationStatsType = () => {
|
|
|
+ if (remainingBedsCount.value === 0) {
|
|
|
+ return 'success';
|
|
|
+ } else if (remainingBedsCount.value > 0) {
|
|
|
+ return 'warning';
|
|
|
+ } else {
|
|
|
+ return 'error';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 递归查找学院节点
|
|
|
+const findCollegeById = (nodes: XgStudentTreeVO[], targetId: string | number): XgStudentTreeVO | null => {
|
|
|
+ for (const node of nodes) {
|
|
|
+ if (node.id === targetId) {
|
|
|
+ return node;
|
|
|
+ }
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ const found = findCollegeById(node.children, targetId);
|
|
|
+ if (found) {
|
|
|
+ return found;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+};
|
|
|
+
|
|
|
// 确认分配
|
|
|
const handleConfirm = async () => {
|
|
|
if (selectedCollegeIds.value.length === 0) {
|
|
|
@@ -369,6 +623,21 @@ const handleConfirm = async () => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 验证学院人数分配
|
|
|
+ const totalAllocated = Object.values(allocationForm.value).reduce((sum, count) => sum + (count || 0), 0);
|
|
|
+ if (totalAllocated !== props.selectedBeds.length) {
|
|
|
+ ElMessage.warning(`床位分配不匹配:已分配 ${totalAllocated} 个,但选择了 ${props.selectedBeds.length} 个床位`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证每个学院都有分配数量
|
|
|
+ // for (const collegeId of selectedCollegeIds.value) {
|
|
|
+ // if (!allocationForm.value[collegeId] || allocationForm.value[collegeId] <= 0) {
|
|
|
+ // ElMessage.warning(`请为学院"${getCollegeName(collegeId)}"指定分配的床位数`);
|
|
|
+ // return;
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+
|
|
|
try {
|
|
|
await ElMessageBox.confirm(`确定要将 ${props.selectedBeds.length} 个床位分配给 ${selectedCollegeIds.value.length} 个学院吗?`, '确认分配', {
|
|
|
confirmButtonText: '确定分配',
|
|
|
@@ -378,10 +647,11 @@ const handleConfirm = async () => {
|
|
|
|
|
|
confirmLoading.value = true;
|
|
|
|
|
|
- // 构建分配参数
|
|
|
+ // 构建分配参数,包含学院人数信息
|
|
|
const allocationData = {
|
|
|
bedIds: props.selectedBeds.map((bed) => bed.id),
|
|
|
collegeIds: selectedCollegeIds.value,
|
|
|
+ collegeStudentCounts: selectedCollegeIds.value.map((collegeId) => allocationForm.value[collegeId]),
|
|
|
buildingId: props.buildingInfo?.id,
|
|
|
buildingType: props.buildingInfo?.buildingType
|
|
|
};
|
|
|
@@ -409,6 +679,7 @@ const handleClose = () => {
|
|
|
searchKey.value = '';
|
|
|
confirmLoading.value = false;
|
|
|
bedTreeData.value = [];
|
|
|
+ allocationForm.value = {}; // 重置人数分配表单
|
|
|
|
|
|
emit('update:modelValue', false);
|
|
|
};
|
|
|
@@ -427,92 +698,22 @@ watch(
|
|
|
// 监听床位选择变化,重新构建树形数据
|
|
|
watch(
|
|
|
() => props.selectedBeds,
|
|
|
- () => {
|
|
|
+ (newBeds, oldBeds) => {
|
|
|
buildBedTreeData();
|
|
|
+
|
|
|
+ // 如果床位被清空,自动清空已选择的学院
|
|
|
+ if (newBeds.length === 0 && oldBeds && oldBeds.length > 0) {
|
|
|
+ selectedCollegeIds.value = [];
|
|
|
+ allocationForm.value = {};
|
|
|
+ nextTick(() => {
|
|
|
+ // 取消所有勾选状态
|
|
|
+ collegeTreeRef.value?.setCheckedKeys([]);
|
|
|
+ });
|
|
|
+ ElMessage.info('床位已清空,已自动清空学院选择');
|
|
|
+ }
|
|
|
},
|
|
|
{ deep: true }
|
|
|
);
|
|
|
-
|
|
|
-// 获取勾选统计信息标题
|
|
|
-const getSelectionStatsTitle = () => {
|
|
|
- if (selectedCollegeIds.value.length === 0) {
|
|
|
- return '未选择学院';
|
|
|
- }
|
|
|
-
|
|
|
- if (props.buildingInfo?.buildingType === '1') {
|
|
|
- return `已选择 ${selectedCollegeIds.value.length} 个学院(男生楼)`;
|
|
|
- } else if (props.buildingInfo?.buildingType === '2') {
|
|
|
- return `已选择 ${selectedCollegeIds.value.length} 个学院(女生楼)`;
|
|
|
- }
|
|
|
- return `已选择 ${selectedCollegeIds.value.length} 个学院`;
|
|
|
-};
|
|
|
-
|
|
|
-// 获取勾选统计信息描述
|
|
|
-const getSelectionStatsDescription = () => {
|
|
|
- if (selectedCollegeIds.value.length === 0) {
|
|
|
- return '请勾选要分配的学院';
|
|
|
- }
|
|
|
-
|
|
|
- // 计算所有勾选学院的学生统计
|
|
|
- let totalStudents = 0;
|
|
|
- let assignedStudents = 0;
|
|
|
-
|
|
|
- selectedCollegeIds.value.forEach((collegeId) => {
|
|
|
- const college = findCollegeById(collegeTreeData.value, collegeId);
|
|
|
- if (college && college.type === 'college') {
|
|
|
- if (props.buildingInfo?.buildingType === '1') {
|
|
|
- // 男生楼,统计男生
|
|
|
- totalStudents += college.maleTotal || 0;
|
|
|
- assignedStudents += college.maleAssigned || 0;
|
|
|
- } else if (props.buildingInfo?.buildingType === '2') {
|
|
|
- // 女生楼,统计女生
|
|
|
- totalStudents += college.femaleTotal || 0;
|
|
|
- assignedStudents += college.femaleAssigned || 0;
|
|
|
- } else {
|
|
|
- // 混合楼,统计总数
|
|
|
- totalStudents += (college.maleTotal || 0) + (college.femaleTotal || 0);
|
|
|
- assignedStudents += (college.maleAssigned || 0) + (college.femaleAssigned || 0);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- if (props.buildingInfo?.buildingType === '1') {
|
|
|
- return `男生总数:${totalStudents} 人,已分配:${assignedStudents} 人,可分配:${totalStudents - assignedStudents} 人`;
|
|
|
- } else if (props.buildingInfo?.buildingType === '2') {
|
|
|
- return `女生总数:${totalStudents} 人,已分配:${assignedStudents} 人,可分配:${totalStudents - assignedStudents} 人`;
|
|
|
- }
|
|
|
- return `学生总数:${totalStudents} 人,已分配:${assignedStudents} 人,可分配:${totalStudents - assignedStudents} 人`;
|
|
|
-};
|
|
|
-
|
|
|
-// 获取勾选统计信息类型
|
|
|
-const getSelectionStatsType = () => {
|
|
|
- if (selectedCollegeIds.value.length === 0) {
|
|
|
- return 'info';
|
|
|
- }
|
|
|
-
|
|
|
- if (props.buildingInfo?.buildingType === '1') {
|
|
|
- return 'success';
|
|
|
- } else if (props.buildingInfo?.buildingType === '2') {
|
|
|
- return 'success';
|
|
|
- }
|
|
|
- return 'warning';
|
|
|
-};
|
|
|
-
|
|
|
-// 递归查找学院节点
|
|
|
-const findCollegeById = (nodes: XgStudentTreeVO[], targetId: string | number): XgStudentTreeVO | null => {
|
|
|
- for (const node of nodes) {
|
|
|
- if (node.id === targetId) {
|
|
|
- return node;
|
|
|
- }
|
|
|
- if (node.children && node.children.length > 0) {
|
|
|
- const found = findCollegeById(node.children, targetId);
|
|
|
- if (found) {
|
|
|
- return found;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return null;
|
|
|
-};
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -710,4 +911,40 @@ const findCollegeById = (nodes: XgStudentTreeVO[], targetId: string | number): X
|
|
|
.stats-icon {
|
|
|
margin-right: 8px;
|
|
|
}
|
|
|
+
|
|
|
+/* 学院人数分配表单样式 */
|
|
|
+.college-allocation {
|
|
|
+ border-top: 1px solid var(--el-border-color-light);
|
|
|
+ padding-top: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.allocation-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.allocation-form {
|
|
|
+ background-color: var(--el-fill-color-lighter);
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.allocation-stats {
|
|
|
+ margin-top: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.allocation-alert {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单验证样式 */
|
|
|
+:deep(.el-form-item.is-error .el-input-number) {
|
|
|
+ border-color: var(--el-color-danger);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-form-item.is-error .el-input-number:focus-within) {
|
|
|
+ border-color: var(--el-color-danger);
|
|
|
+ box-shadow: 0 0 0 1px var(--el-color-danger-light-8);
|
|
|
+}
|
|
|
</style>
|