This commit is contained in:
zlwu committed 2025-11-13 23:30:05 +08:00
1 parent f4a1eac9ed
commit 4d48300a1d
67 files changed
+1211 -614

No files matched your search

+292 -6
View File
@@ -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; /* 减少与搜索框的间距 */
}