提交
This commit is contained in:
1 parent
f4a1eac9ed
commit
4d48300a1d
67 files changed
+1211
-614
No files matched your search
@@ -11,6 +11,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="deptName" placeholder="请输入院系名称" prefix-icon="Search" clearable />
|
||||
<div class="class-status-filter mt-2">
|
||||
<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"
|
||||
@@ -23,7 +30,22 @@
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@check="handleDeptCheck"
|
||||
/>
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="dept-tree-node-label">
|
||||
<span class="dept-tree-node-text">{{ data.label }}</span>
|
||||
<el-tag
|
||||
v-if="isClassNode(data) && data.classStatus"
|
||||
size="small"
|
||||
effect="light"
|
||||
:type="getClassStatusTagType(data.classStatus)"
|
||||
class="dept-tree-node-tag"
|
||||
>
|
||||
{{ getClassStatusLabel(data.classStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
@@ -295,16 +317,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, toRefs, watchEffect, onMounted, computed, getCurrentInstance } from 'vue';
|
||||
import { ref, reactive, toRefs, watch, watchEffect, onMounted, computed, getCurrentInstance, nextTick } from 'vue';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import ElMessageBox from 'element-plus/es/components/message-box/index';
|
||||
import { Search, Refresh, Setting, Download, List, Grid, View, Picture, Male, Female, Loading } from '@element-plus/icons-vue';
|
||||
import AdvancedSearchDialog from './components/AdvancedSearchDialog.vue';
|
||||
import StudentDetailDialog from './components/StudentDetailDialog.vue';
|
||||
import api from '@/api/xg/xsjcxx/stuRoster/index';
|
||||
import { listClassMsg } from '@/api/xg/xsjcxx/classMsg/classMsg/index';
|
||||
import type { ClassMsgVO } from '@/api/xg/xsjcxx/classMsg/types';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
const { xg_class_status } = toRefs<any>(proxy?.useDict('xg_class_status') || {});
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
@@ -314,6 +339,7 @@ const showAdvancedSearch = ref(false);
|
||||
const deptName = ref('');
|
||||
const deptTreeRef = ref();
|
||||
const selectedDepts = ref<number[]>([]);
|
||||
const classStatusFilter = ref<'all' | 'normal'>('all');
|
||||
|
||||
// 学生照片缓存
|
||||
const studentPhotoCache = ref<Map<string, string>>(new Map());
|
||||
@@ -333,6 +359,11 @@ const tableMaxHeight = computed(() => {
|
||||
return baseHeight - headerHeight - searchHeight - toolbarHeight - paginationHeight - padding - cardPadding;
|
||||
});
|
||||
|
||||
const normalClassStatusLabel = computed(() => {
|
||||
const label = getClassStatusLabel('1');
|
||||
return label || '正常';
|
||||
});
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
@@ -449,8 +480,14 @@ const student_status = ref([
|
||||
]);
|
||||
|
||||
// 部门树数据
|
||||
const rawDeptOptions = ref<any[]>([]);
|
||||
const deptOptions = ref([]);
|
||||
|
||||
// 班级状态缓存
|
||||
const classStatusMap = new Map<string, string>();
|
||||
let classStatusLoaded = false;
|
||||
let classStatusLoading = false;
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref([
|
||||
{ key: 0, label: '学号', visible: true },
|
||||
@@ -949,6 +986,204 @@ const isPhotoLoading = (student: any): boolean => {
|
||||
return photoLoadingCache.value.get(photoPath) || false;
|
||||
};
|
||||
|
||||
const getClassStatusLabel = (status: string): string => {
|
||||
if (!status) {
|
||||
return '';
|
||||
}
|
||||
const options = (xg_class_status?.value as any[]) || [];
|
||||
const found = options.find((item) => item?.value === status);
|
||||
return (found && found.label) || status;
|
||||
};
|
||||
|
||||
const getClassStatusTagType = (status: string): 'success' | 'warning' | 'info' | 'primary' | 'danger' => {
|
||||
const statusTypeMap: Record<string, 'success' | 'warning' | 'info' | 'primary' | 'danger'> = {
|
||||
'1': 'success',
|
||||
'2': 'warning',
|
||||
'3': 'primary',
|
||||
'4': 'danger',
|
||||
'5': 'info'
|
||||
};
|
||||
return statusTypeMap[status] || 'info';
|
||||
};
|
||||
|
||||
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 = {
|
||||
pageNum: 1,
|
||||
pageSize: 9999,
|
||||
params: {}
|
||||
};
|
||||
const res = await listClassMsg(params);
|
||||
const rows: ClassMsgVO[] = (res && ((res as any).rows || (res as any).data?.rows)) || [];
|
||||
classStatusMap.clear();
|
||||
rows.forEach((item) => {
|
||||
if (item?.id === undefined || item?.id === null) {
|
||||
return;
|
||||
}
|
||||
const statusValue = item.classStatus;
|
||||
if (statusValue === undefined || statusValue === null || statusValue === '') {
|
||||
return;
|
||||
}
|
||||
classStatusMap.set(String(item.id), String(statusValue));
|
||||
});
|
||||
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 buildDeptTreeWithStatus = (nodes: any[]): any[] => {
|
||||
return (nodes || []).map((node) => {
|
||||
const enhanced: any = { ...node };
|
||||
if (Array.isArray(node?.children) && node.children.length > 0) {
|
||||
enhanced.children = buildDeptTreeWithStatus(node.children);
|
||||
} else {
|
||||
enhanced.children = [];
|
||||
}
|
||||
const baseLabel = enhanced.originalLabel ?? enhanced.label ?? '';
|
||||
enhanced.originalLabel = baseLabel;
|
||||
|
||||
if (isClassNode(enhanced)) {
|
||||
const status = resolveClassStatus(enhanced);
|
||||
if (status) {
|
||||
const statusLabel = getClassStatusLabel(status);
|
||||
enhanced.classStatus = status;
|
||||
enhanced.classStatusLabel = statusLabel;
|
||||
}
|
||||
}
|
||||
enhanced.label = baseLabel;
|
||||
return enhanced;
|
||||
});
|
||||
};
|
||||
|
||||
const collectNodeIds = (nodes: any[]): Set<string> => {
|
||||
const ids = new Set<string>();
|
||||
const traverse = (list: any[]) => {
|
||||
(list || []).forEach((item) => {
|
||||
if (item?.id !== undefined && item?.id !== null) {
|
||||
ids.add(String(item.id));
|
||||
}
|
||||
if (Array.isArray(item?.children) && item.children.length > 0) {
|
||||
traverse(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
traverse(nodes);
|
||||
return ids;
|
||||
};
|
||||
|
||||
const filterDeptTreeByStatus = (nodes: any[], status?: string): any[] => {
|
||||
const targetStatus = status?.trim();
|
||||
return (nodes || [])
|
||||
.map((node) => {
|
||||
const filtered: any = { ...node };
|
||||
if (Array.isArray(node?.children) && node.children.length > 0) {
|
||||
filtered.children = filterDeptTreeByStatus(node.children, status);
|
||||
} else {
|
||||
filtered.children = [];
|
||||
}
|
||||
|
||||
if (targetStatus && isClassNode(filtered)) {
|
||||
return filtered.classStatus === targetStatus ? filtered : null;
|
||||
}
|
||||
|
||||
if (targetStatus && filtered.children.length === 0 && !isClassNode(filtered)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filtered;
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
};
|
||||
|
||||
const findFirstSelectableNode = (nodes: any[]): any | null => {
|
||||
for (const node of nodes || []) {
|
||||
if (!node?.disabled) {
|
||||
return node;
|
||||
}
|
||||
if (Array.isArray(node?.children) && node.children.length > 0) {
|
||||
const child = findFirstSelectableNode(node.children);
|
||||
if (child) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const updateDeptOptionsWithStatus = () => {
|
||||
const checkedKeys = Array.isArray(selectedDepts.value) ? [...selectedDepts.value] : [];
|
||||
const currentKey = deptTreeRef.value?.getCurrentKey?.();
|
||||
|
||||
let processedTree = buildDeptTreeWithStatus(rawDeptOptions.value);
|
||||
|
||||
if (classStatusFilter.value === 'normal') {
|
||||
processedTree = filterDeptTreeByStatus(processedTree, '1');
|
||||
}
|
||||
|
||||
const visibleIds = collectNodeIds(processedTree);
|
||||
const validCheckedKeys = checkedKeys.filter((key) => visibleIds.has(String(key)));
|
||||
const selectionChanged = validCheckedKeys.length !== checkedKeys.length;
|
||||
|
||||
selectedDepts.value = validCheckedKeys;
|
||||
queryParams.deptIds = validCheckedKeys;
|
||||
|
||||
deptOptions.value = processedTree;
|
||||
|
||||
nextTick(() => {
|
||||
deptTreeRef.value?.setCheckedKeys(validCheckedKeys);
|
||||
if (currentKey && visibleIds.has(String(currentKey))) {
|
||||
deptTreeRef.value?.setCurrentKey(currentKey);
|
||||
} else if (!currentKey) {
|
||||
const fallbackNode = findFirstSelectableNode(processedTree);
|
||||
if (fallbackNode?.id !== undefined) {
|
||||
deptTreeRef.value?.setCurrentKey(fallbackNode.id);
|
||||
}
|
||||
}
|
||||
if (selectionChanged) {
|
||||
getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleClassStatusFilterChange = () => {
|
||||
updateDeptOptionsWithStatus();
|
||||
};
|
||||
|
||||
// 监听部门名称变化
|
||||
watchEffect(() => {
|
||||
if (deptTreeRef.value) {
|
||||
@@ -956,37 +1191,56 @@ watchEffect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => xg_class_status?.value,
|
||||
() => {
|
||||
if (rawDeptOptions.value.length > 0) {
|
||||
updateDeptOptionsWithStatus();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 递归处理树数据,将所有节点的disabled属性设置为false
|
||||
const processTreeData = (data: any[]): any[] => {
|
||||
return data.map((node) => ({
|
||||
...node,
|
||||
disabled: false, // 强制设置为false,允许所有节点勾选
|
||||
originalLabel: node.originalLabel ?? node.label,
|
||||
children: node.children ? processTreeData(node.children) : undefined
|
||||
}));
|
||||
};
|
||||
|
||||
const handleDeptTreeData = async (data: any[]) => {
|
||||
rawDeptOptions.value = processTreeData(data);
|
||||
await ensureClassStatusMap();
|
||||
updateDeptOptionsWithStatus();
|
||||
};
|
||||
|
||||
/** 查询部门下拉树结构 */
|
||||
const getDeptTree = async () => {
|
||||
try {
|
||||
const res = await api.XgDeptTreeSelect(101);
|
||||
// XgDeptTreeSelect返回的数据结构与deptTreeSelect不同,需要进行适当处理
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
deptOptions.value = processTreeData(res.data);
|
||||
await handleDeptTreeData(res.data);
|
||||
} else {
|
||||
console.warn('部门树数据格式不符合预期');
|
||||
// 回退到默认处理
|
||||
const backup = await api.deptTreeSelect();
|
||||
deptOptions.value = processTreeData(backup.data.data);
|
||||
await handleDeptTreeData(backup.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取部门树失败:', error);
|
||||
// 如果API调用失败,可以使用默认的deptTreeSelect作为备选
|
||||
try {
|
||||
const backup = await api.deptTreeSelect();
|
||||
deptOptions.value = processTreeData(backup.data.data);
|
||||
await handleDeptTreeData(backup.data.data);
|
||||
} catch (backupError) {
|
||||
console.error('备选API也失败:', backupError);
|
||||
ElMessage.error('获取部门树失败,请重试');
|
||||
rawDeptOptions.value = [];
|
||||
deptOptions.value = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1028,6 +1282,10 @@ onMounted(() => {
|
||||
padding-bottom: 20px; /* 增加底部内边距,为按钮留出空间 */
|
||||
}
|
||||
|
||||
.left-panel :deep(.el-card__header) {
|
||||
padding: 9px 21px !important;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
@@ -1035,6 +1293,18 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.class-status-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.class-status-filter__label {
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dept-tree {
|
||||
height: calc(100vh - 280px); /* 距离视窗底部100px,进一步减少高度 */
|
||||
overflow-y: auto;
|
||||
@@ -1079,6 +1349,22 @@ onMounted(() => {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.dept-tree-node-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dept-tree-node-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dept-tree-node-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 确保展开图标有足够空间 */
|
||||
.dept-tree :deep(.el-tree-node__expand-icon) {
|
||||
margin-right: 4px;
|
||||
@@ -1095,7 +1381,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0 !important; /* 减少头部内边距 */
|
||||
padding: 0 !important;
|
||||
margin-bottom: 6px; /* 减少与搜索框的间距 */
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user