提交
This commit is contained in:
1 parent
7d4bfaecb3
commit
f4a1eac9ed
3 files changed
+320
-449
No files matched your search
@@ -5,6 +5,13 @@
|
||||
<div class="left-panel">
|
||||
<el-card shadow="hover">
|
||||
<el-input v-model="deptName" placeholder="请输入院系名称" prefix-icon="Search" clearable />
|
||||
<div class="class-status-filter">
|
||||
<span class="class-status-filter__label">班级状态:</span>
|
||||
<el-radio-group v-model="classStatusFilter" size="small" @change="handleClassStatusFilterChange">
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
<el-radio-button label="normal">{{ normalClassStatusLabel }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
class="mt-2 dept-tree"
|
||||
@@ -16,7 +23,16 @@
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="dept-tree-node">
|
||||
<span class="dept-tree-node__label">{{ data.label }}</span>
|
||||
<el-tag v-if="isClassNode(data) && data.classStatus" size="small" effect="light" :type="getClassStatusTagType(data.classStatus)">
|
||||
{{ getClassStatusLabel(data.classStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
@@ -1497,8 +1513,10 @@ import api from '@/api/xg/xsjcxx/studentMsg/index';
|
||||
import { downloadImportTemplate } from '@/api/xg/xsjcxx/studentMsg/studentMsg/index';
|
||||
import { StudentForm, StudentQuery, StudentVO } from '@/api/xg/xsjcxx/studentMsg/types';
|
||||
import { StudentMsgForm } from '@/api/xg/xsjcxx/studentMsg/studentMsg/types';
|
||||
import { listClassMsg } from '@/api/xg/xsjcxx/classMsg/classMsg';
|
||||
import type { ClassMsgQuery, ClassMsgVO } from '@/api/xg/xsjcxx/classMsg/classMsg/types';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getCurrentInstance, ref, reactive, toRefs, watchEffect, onMounted, computed } from 'vue';
|
||||
import { getCurrentInstance, ref, reactive, toRefs, watchEffect, onMounted, computed, watch, nextTick } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { to } from 'await-to-js';
|
||||
import { globalHeaders } from '@/utils/request';
|
||||
@@ -1529,7 +1547,8 @@ const {
|
||||
zsjy_education_level,
|
||||
xg_training_mode,
|
||||
admission_category,
|
||||
student_status
|
||||
student_status,
|
||||
xg_class_status
|
||||
} = toRefs<any>(
|
||||
proxy?.useDict(
|
||||
'wenhuachengdu',
|
||||
@@ -1549,7 +1568,8 @@ const {
|
||||
'zsjy_education_level',
|
||||
'xg_training_mode',
|
||||
'admission_category',
|
||||
'student_status'
|
||||
'student_status',
|
||||
'xg_class_status'
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1595,6 +1615,26 @@ const getAdmissionCategoryLabel = (value: string | number) => {
|
||||
return dict ? dict.label : value || '未知';
|
||||
};
|
||||
|
||||
/** 班级状态码表映射 */
|
||||
const getClassStatusLabel = (value: string | number) => {
|
||||
const options = xg_class_status?.value || [];
|
||||
const dict = options.find((item: any) => item.value === String(value));
|
||||
return dict ? dict.label : value || '未知';
|
||||
};
|
||||
|
||||
/** 班级状态标签类型 */
|
||||
const getClassStatusTagType = (value: string | number): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
|
||||
const options = xg_class_status?.value || [];
|
||||
const dict = options.find((item: any) => item.value === String(value));
|
||||
const tagType = dict?.elTagType || dict?.listClass;
|
||||
if (tagType && ['primary', 'success', 'info', 'warning', 'danger'].includes(tagType)) {
|
||||
return tagType;
|
||||
}
|
||||
return 'info';
|
||||
};
|
||||
|
||||
const normalClassStatusLabel = computed(() => getClassStatusLabel('1'));
|
||||
|
||||
/** 学籍状态码表映射 */
|
||||
const getStudentStatusLabel = (value: string | number) => {
|
||||
const dict = student_status.value.find((item: any) => item.value === String(value));
|
||||
@@ -1614,6 +1654,7 @@ const total = ref(0);
|
||||
const dateRange = ref<[string, string]>(['', '']);
|
||||
const deptName = ref('');
|
||||
const deptOptions = ref([]);
|
||||
const rawDeptOptions = ref<any[]>([]);
|
||||
const majorOptions = ref([]);
|
||||
const classOptions = ref([]);
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
@@ -1621,6 +1662,10 @@ const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const currentSelectedNode = ref<any>(null); // 当前选中的部门节点
|
||||
const roomCache = ref<Map<string | number, string>>(new Map()); // 房间名称缓存
|
||||
const bedCache = ref<Map<string | number, string>>(new Map()); // 床位名称缓存
|
||||
const classStatusFilter = ref<'all' | 'normal'>('all');
|
||||
const classStatusMap = new Map<string | number, string>();
|
||||
let classStatusLoaded = false;
|
||||
let classStatusLoading = false;
|
||||
|
||||
// 计算属性:判断是否显示新增按钮(只有班级节点才显示)
|
||||
const showAddButton = computed(() => {
|
||||
@@ -1893,6 +1938,221 @@ const getDormBuildingName = (id: string | number) => {
|
||||
return findBuildingName(dormBuildingOptions.value, id) || String(id);
|
||||
};
|
||||
|
||||
/** 获取节点deptCategory */
|
||||
const getDeptCategory = (node: any) => {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
const category = node.deptCategory ?? node.dept_category ?? node.params?.deptCategory;
|
||||
if (category && !node.deptCategory) {
|
||||
node.deptCategory = category;
|
||||
}
|
||||
return category;
|
||||
};
|
||||
|
||||
/** 是否班级节点 */
|
||||
const isClassNode = (node: any) => {
|
||||
const category = getDeptCategory(node);
|
||||
return typeof category === 'string' && category.toLowerCase().startsWith('bj-');
|
||||
};
|
||||
|
||||
/** 加载班级状态映射 */
|
||||
const ensureClassStatusMap = async () => {
|
||||
if (classStatusLoaded || classStatusLoading) {
|
||||
return;
|
||||
}
|
||||
classStatusLoading = true;
|
||||
try {
|
||||
const params: ClassMsgQuery = {
|
||||
pageNum: 1,
|
||||
pageSize: 9999,
|
||||
params: {}
|
||||
};
|
||||
const res = await listClassMsg(params);
|
||||
const rows: ClassMsgVO[] = (res && (res.rows || res.data?.rows)) || [];
|
||||
rows.forEach((item) => {
|
||||
if (item?.id === undefined || item?.id === null) {
|
||||
return;
|
||||
}
|
||||
const status = item?.classStatus;
|
||||
if (status === undefined || status === null || status === '') {
|
||||
return;
|
||||
}
|
||||
classStatusMap.set(String(item.id), String(status));
|
||||
});
|
||||
classStatusLoaded = true;
|
||||
} catch (error) {
|
||||
console.error('加载班级状态失败:', error);
|
||||
} finally {
|
||||
classStatusLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 解析节点班级状态 */
|
||||
const resolveClassStatus = (node: any): string | undefined => {
|
||||
const existing = node?.classStatus ?? node?.class_status;
|
||||
if (existing !== undefined && existing !== null && existing !== '') {
|
||||
return String(existing);
|
||||
}
|
||||
const deptId = node?.id ?? node?.deptId;
|
||||
if (deptId === undefined || deptId === null) {
|
||||
return undefined;
|
||||
}
|
||||
return classStatusMap.get(String(deptId));
|
||||
};
|
||||
|
||||
/** 深拷贝部门树节点 */
|
||||
const cloneDeptTreeNodes = (nodes: any[]): any[] => {
|
||||
return (nodes || []).map((node) => {
|
||||
const cloned: any = { ...node };
|
||||
if (Array.isArray(node.children) && node.children.length > 0) {
|
||||
cloned.children = cloneDeptTreeNodes(node.children);
|
||||
} else {
|
||||
cloned.children = [];
|
||||
}
|
||||
return cloned;
|
||||
});
|
||||
};
|
||||
|
||||
/** 增强树节点(补充班级状态) */
|
||||
const enhanceDeptTreeNodes = (nodes: any[]): any[] => {
|
||||
return (nodes || []).map((node) => {
|
||||
const enhanced: any = { ...node };
|
||||
if (Array.isArray(enhanced.children) && enhanced.children.length > 0) {
|
||||
enhanced.children = enhanceDeptTreeNodes(enhanced.children);
|
||||
}
|
||||
if (isClassNode(enhanced)) {
|
||||
const status = resolveClassStatus(enhanced);
|
||||
if (status !== undefined) {
|
||||
enhanced.classStatus = status;
|
||||
}
|
||||
}
|
||||
return enhanced;
|
||||
});
|
||||
};
|
||||
|
||||
/** 根据状态过滤班级节点 */
|
||||
const filterDeptTreeByStatus = (nodes: any[], status?: string): any[] => {
|
||||
const targetStatus = status ?? '';
|
||||
return (nodes || [])
|
||||
.map((node) => {
|
||||
const cloned: any = { ...node };
|
||||
if (Array.isArray(node.children) && node.children.length > 0) {
|
||||
cloned.children = filterDeptTreeByStatus(node.children, status);
|
||||
} else {
|
||||
cloned.children = [];
|
||||
}
|
||||
if (targetStatus && isClassNode(cloned)) {
|
||||
if (cloned.classStatus !== targetStatus) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
})
|
||||
.filter((item) => item !== null) as any[];
|
||||
};
|
||||
|
||||
/** 根据id查找节点 */
|
||||
const findDeptNodeById = (nodes: any[], id: string | number): any | undefined => {
|
||||
for (const node of nodes || []) {
|
||||
if (node?.id === id) {
|
||||
return node;
|
||||
}
|
||||
if (Array.isArray(node.children) && node.children.length > 0) {
|
||||
const found = findDeptNodeById(node.children, id);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** 应用班级状态过滤 */
|
||||
const applyClassStatusFilter = (preserveSelection = true) => {
|
||||
const baseTree = rawDeptOptions.value || [];
|
||||
let filteredTree: any[] = [];
|
||||
if (classStatusFilter.value === 'normal') {
|
||||
filteredTree = filterDeptTreeByStatus(baseTree, '1');
|
||||
} else {
|
||||
filteredTree = cloneDeptTreeNodes(baseTree);
|
||||
}
|
||||
deptOptions.value = filteredTree;
|
||||
|
||||
if (preserveSelection) {
|
||||
nextTick(() => {
|
||||
const selectedId = currentSelectedNode.value?.id;
|
||||
if (!selectedId) {
|
||||
return;
|
||||
}
|
||||
const exists = findDeptNodeById(filteredTree, selectedId);
|
||||
if (exists) {
|
||||
deptTreeRef.value?.setCurrentKey(selectedId);
|
||||
currentSelectedNode.value = exists;
|
||||
} else {
|
||||
currentSelectedNode.value = null;
|
||||
queryParams.deptId = undefined;
|
||||
deptTreeRef.value?.setCurrentKey(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (deptTreeRef.value && deptName.value) {
|
||||
deptTreeRef.value.filter(deptName.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 处理状态过滤变更 */
|
||||
const handleClassStatusFilterChange = async () => {
|
||||
await ensureClassStatusMap();
|
||||
const previousSelectedId = currentSelectedNode.value?.id;
|
||||
applyClassStatusFilter();
|
||||
nextTick(() => {
|
||||
const currentSelectedId = currentSelectedNode.value?.id;
|
||||
if (currentSelectedId) {
|
||||
if (currentSelectedId !== previousSelectedId) {
|
||||
handleQuery();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (deptOptions.value && deptOptions.value.length > 0) {
|
||||
setDefaultRootNode(deptOptions.value);
|
||||
} else {
|
||||
queryParams.deptId = undefined;
|
||||
getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 提取接口返回的树形数据 */
|
||||
const extractDeptTreeData = (payload: any): any[] => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload;
|
||||
}
|
||||
if (Array.isArray(payload?.data)) {
|
||||
return payload.data;
|
||||
}
|
||||
if (Array.isArray(payload?.data?.data)) {
|
||||
return payload.data.data;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
/** 构建部门树 */
|
||||
const rebuildDeptTree = async (treeData: any[], resetSelection = true) => {
|
||||
await ensureClassStatusMap();
|
||||
const enhanced = enhanceDeptTreeNodes(treeData);
|
||||
rawDeptOptions.value = enhanced;
|
||||
applyClassStatusFilter(!resetSelection);
|
||||
if (resetSelection) {
|
||||
nextTick(() => {
|
||||
setDefaultRootNode(deptOptions.value);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** 根据房间ID获取房间名称 */
|
||||
const getRoomName = async (roomId: string | number): Promise<string> => {
|
||||
if (!roomId) return '';
|
||||
@@ -2129,26 +2389,25 @@ const getList = async () => {
|
||||
const getDeptTree = async () => {
|
||||
try {
|
||||
const res = await api.XgDeptTreeSelect(101);
|
||||
// XgDeptTreeSelect返回的数据结构与deptTreeSelect不同,需要进行适当处理
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
deptOptions.value = res.data;
|
||||
// 设置根节点为默认选中节点
|
||||
setDefaultRootNode(res.data);
|
||||
} else {
|
||||
console.warn('部门树数据格式不符合预期');
|
||||
// 回退到默认处理
|
||||
const backup = await api.deptTreeSelect();
|
||||
deptOptions.value = backup.data.data;
|
||||
// 设置根节点为默认选中节点
|
||||
setDefaultRootNode(backup.data.data);
|
||||
const treeData = extractDeptTreeData(res);
|
||||
if (treeData.length > 0) {
|
||||
await rebuildDeptTree(treeData, true);
|
||||
return;
|
||||
}
|
||||
console.warn('部门树数据格式不符合预期,尝试使用备选接口');
|
||||
const backup = await api.deptTreeSelect();
|
||||
const backupTree = extractDeptTreeData(backup);
|
||||
if (backupTree.length > 0) {
|
||||
await rebuildDeptTree(backupTree, true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取部门树失败:', error);
|
||||
// 如果API调用失败,可以使用默认的deptTreeSelect作为备选
|
||||
const backup = await api.deptTreeSelect();
|
||||
deptOptions.value = backup.data.data;
|
||||
// 设置根节点为默认选中节点
|
||||
setDefaultRootNode(backup.data.data);
|
||||
const backupTree = extractDeptTreeData(backup);
|
||||
if (backupTree.length > 0) {
|
||||
await rebuildDeptTree(backupTree, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2270,6 +2529,10 @@ const resetQuery = () => {
|
||||
queryParams.studentNo = '';
|
||||
queryParams.phone = '';
|
||||
queryParams.studentStatus = '';
|
||||
if (classStatusFilter.value !== 'all') {
|
||||
classStatusFilter.value = 'all';
|
||||
applyClassStatusFilter(false);
|
||||
}
|
||||
|
||||
// 重置到默认根节点
|
||||
if (deptOptions.value && deptOptions.value.length > 0) {
|
||||
@@ -2959,6 +3222,23 @@ onMounted(() => {
|
||||
min-width: 330px;
|
||||
}
|
||||
|
||||
.class-status-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.class-status-filter__label {
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-panel .w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 强制覆盖el-tree的默认样式 */
|
||||
.dept-tree :deep(.el-tree-node) {
|
||||
white-space: nowrap !important;
|
||||
@@ -3004,6 +3284,17 @@ onMounted(() => {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.dept-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dept-tree-node__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 右侧面板 - 占据剩余空间 */
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
|
||||
Reference in new issue
Block a user