|
|
@@ -177,15 +177,38 @@
|
|
|
<!-- 收集范围设置对话框 -->
|
|
|
<el-dialog title="设置收集范围" v-model="scopeDialog.visible" width="500px" append-to-body @close="handleScopeDialogClose">
|
|
|
<div class="scope-content">
|
|
|
+ <!-- 过滤按钮区域 -->
|
|
|
+ <div class="filter-buttons">
|
|
|
+ <el-radio-group v-model="classStatusFilter" size="small" @change="handleFilterChange">
|
|
|
+ <el-radio-button label="all">全部</el-radio-button>
|
|
|
+ <el-radio-button label="normal">正常</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </div>
|
|
|
<el-tree
|
|
|
ref="scopeTreeRef"
|
|
|
- :data="deptTreeData"
|
|
|
+ :data="filteredTreeData"
|
|
|
show-checkbox
|
|
|
default-expand-all
|
|
|
node-key="id"
|
|
|
:props="{ children: 'children', label: 'label' }"
|
|
|
:check-strictly="false"
|
|
|
- />
|
|
|
+ >
|
|
|
+ <template #default="{ data }">
|
|
|
+ <span class="scope-node">
|
|
|
+ <span class="scope-label">{{ data.label }}</span>
|
|
|
+ <dict-tag
|
|
|
+ v-if="isClassNode(data) && data.classStatus !== undefined && data.classStatus !== null && data.classStatus !== ''"
|
|
|
+ :options="classStatusOptions"
|
|
|
+ :value="data.classStatus"
|
|
|
+ class="scope-tag"
|
|
|
+ />
|
|
|
+ <!-- 临时调试信息(开发时可见,生产环境可删除) -->
|
|
|
+ <!-- <span v-if="isClassNode(data)" style="font-size: 10px; color: #999; margin-left: 4px;">
|
|
|
+ [{{ data.deptCategory || '无category' }}|{{ data.classStatus || '无状态' }}]
|
|
|
+ </span> -->
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
</div>
|
|
|
<template #footer>
|
|
|
<div class="dialog-footer">
|
|
|
@@ -253,13 +276,27 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup name="StuColBatch" lang="ts">
|
|
|
+import { ref, reactive, computed, toRefs, getCurrentInstance, onMounted, nextTick } from 'vue';
|
|
|
import { listStuColBatch, getStuColBatch, delStuColBatch, addStuColBatch, updateStuColBatch } from '@/api/xg/xsjcxx/XgStuColBatch/stuColBatch';
|
|
|
import { StuColBatchVO, StuColBatchQuery, StuColBatchForm } from '@/api/xg/xsjcxx/XgStuColBatch/stuColBatch/types';
|
|
|
import { setScope, getScope, getScopeDetail } from '@/api/xg/xsjcxx/stuColBatchClass/stuColBatchClass';
|
|
|
+import { listClassMsg } from '@/api/xg/xsjcxx/classMsg/classMsg';
|
|
|
+import type { ClassMsgQuery, ClassMsgVO } from '@/api/xg/xsjcxx/classMsg/types';
|
|
|
import request from '@/utils/request';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
+const { xg_class_status } = toRefs<any>(proxy?.useDict('xg_class_status') || {});
|
|
|
+const classStatusOptions = computed(() => {
|
|
|
+ const options = xg_class_status?.value || [];
|
|
|
+ // 调试日志:检查字典数据是否加载
|
|
|
+ if (options.length === 0) {
|
|
|
+ console.warn('[班级状态] 字典数据未加载,xg_class_status 为空');
|
|
|
+ } else {
|
|
|
+ console.log(`[班级状态] 字典数据已加载,共 ${options.length} 个选项`);
|
|
|
+ }
|
|
|
+ return options;
|
|
|
+});
|
|
|
|
|
|
const stuColBatchList = ref<StuColBatchVO[]>([]);
|
|
|
const buttonLoading = ref(false);
|
|
|
@@ -285,8 +322,14 @@ const scopeDialog = reactive({
|
|
|
const scopeButtonLoading = ref(false);
|
|
|
const currentBatch = ref<StuColBatchVO | null>(null);
|
|
|
const deptTreeData = ref<any[]>([]);
|
|
|
+const originalTreeData = ref<any[]>([]); // 原始树数据(未过滤)
|
|
|
const checkedDeptIds = ref<number[]>([]);
|
|
|
const scopeTreeRef = ref();
|
|
|
+// 班级状态过滤:'all' 全部, 'normal' 正常
|
|
|
+const classStatusFilter = ref<'all' | 'normal'>('all');
|
|
|
+// 班级状态缓存
|
|
|
+const classStatusCache = new Map<string, string>();
|
|
|
+let classStatusLoaded = false;
|
|
|
|
|
|
// 查看详细弹窗相关数据
|
|
|
const viewDialog = reactive({
|
|
|
@@ -436,6 +479,8 @@ const handleScope = async (row: StuColBatchVO) => {
|
|
|
currentBatch.value = row;
|
|
|
// 先清空勾选状态
|
|
|
checkedDeptIds.value = [];
|
|
|
+ // 重置过滤状态为"全部"
|
|
|
+ classStatusFilter.value = 'all';
|
|
|
|
|
|
await loadDeptTree();
|
|
|
await loadCurrentScope(row.id);
|
|
|
@@ -448,10 +493,128 @@ const handleScope = async (row: StuColBatchVO) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+/** 加载班级状态缓存 */
|
|
|
+const ensureClassStatusCache = async () => {
|
|
|
+ if (classStatusLoaded) {
|
|
|
+ console.log('[班级状态] 缓存已加载,跳过');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const params: ClassMsgQuery = {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 9999,
|
|
|
+ params: {}
|
|
|
+ };
|
|
|
+ const res = await listClassMsg(params);
|
|
|
+ const rows: ClassMsgVO[] = res?.rows || [];
|
|
|
+ console.log(`[班级状态] 加载班级数据,共 ${rows.length} 条`);
|
|
|
+
|
|
|
+ // 建立缓存映射:sys_dept.dept_id = xg_class_msg.id -> class_status
|
|
|
+ let cacheCount = 0;
|
|
|
+ rows.forEach((item) => {
|
|
|
+ const status = item?.classStatus;
|
|
|
+ if (status === null || status === undefined || status === '') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const statusStr = String(status);
|
|
|
+ // 关联关系:sys_dept.dept_id = xg_class_msg.id
|
|
|
+ // 所以使用 xg_class_msg.id 作为 key,对应 sys_dept.dept_id
|
|
|
+ if (item.id !== undefined && item.id !== null) {
|
|
|
+ classStatusCache.set(`deptId:${String(item.id)}`, statusStr);
|
|
|
+ cacheCount++;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ classStatusLoaded = true;
|
|
|
+ console.log(`[班级状态] 缓存建立完成,共 ${cacheCount} 条有效状态数据`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载班级状态失败:', error);
|
|
|
+ ElMessage.warning('加载班级状态失败,班级状态标签可能无法显示');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 判断是否是班级节点 */
|
|
|
+const isClassNode = (data: any) => {
|
|
|
+ // 方法1:通过 deptCategory 字段判断(优先)
|
|
|
+ const category = data?.deptCategory ?? data?.dept_category ?? data?.params?.deptCategory;
|
|
|
+ if (category && typeof category === 'string' && category.toLowerCase().startsWith('bj-')) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方法2:通过名称模式判断(备用方案)
|
|
|
+ // 如果节点是叶子节点(没有children或children为空),且名称符合"名称+3位数字"格式,则认为是班级节点
|
|
|
+ const isLeaf = !data?.children || data.children.length === 0;
|
|
|
+ if (isLeaf && data?.label && typeof data.label === 'string') {
|
|
|
+ // 匹配类似"西餐运营251"、"中烹252"这种格式(名称+3位数字结尾)
|
|
|
+ const nameMatch = /^.+?\d{3}$/.test(data.label);
|
|
|
+ if (nameMatch) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
+/** 解析节点班级状态 */
|
|
|
+const resolveClassStatus = (node: any): string | undefined => {
|
|
|
+ // 判断是否是班级节点
|
|
|
+ if (!isClassNode(node)) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 通过 sys_dept.dept_id 关联 xg_class_msg.id 获取 class_status
|
|
|
+ // 关联关系:sys_dept.dept_id = xg_class_msg.id
|
|
|
+ const deptId = node.id ?? node.deptId;
|
|
|
+ if (deptId === undefined || deptId === null) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ const key = `deptId:${String(deptId)}`;
|
|
|
+ if (classStatusCache.has(key)) {
|
|
|
+ const status = classStatusCache.get(key);
|
|
|
+ return status;
|
|
|
+ }
|
|
|
+
|
|
|
+ return undefined;
|
|
|
+};
|
|
|
+
|
|
|
+/** 增强部门树节点(添加班级状态) */
|
|
|
+const enhanceDeptNodes = (nodes: any[]): any[] => {
|
|
|
+ return (nodes || []).map((node) => {
|
|
|
+ const enhanced = { ...node };
|
|
|
+ const category = enhanced.deptCategory ?? enhanced.dept_category ?? enhanced.params?.deptCategory;
|
|
|
+ if (category) {
|
|
|
+ enhanced.deptCategory = category;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断是否是班级节点并获取状态
|
|
|
+ const isClass = isClassNode(enhanced);
|
|
|
+ const status = resolveClassStatus(enhanced);
|
|
|
+
|
|
|
+ if (status) {
|
|
|
+ enhanced.classStatus = status;
|
|
|
+ // 调试日志:记录成功添加状态的节点
|
|
|
+ console.log(`[班级状态] 为节点添加状态: id=${enhanced.id}, label=${enhanced.label}, category=${category || '名称模式'}, status=${status}`);
|
|
|
+ } else if (isClass) {
|
|
|
+ // 调试日志:记录是班级但没有状态的节点
|
|
|
+ console.warn(
|
|
|
+ `[班级状态] 班级节点未找到状态: id=${enhanced.id}, label=${enhanced.label}, category=${category || '名称模式'}, deptId=${enhanced.id}`
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(enhanced.children) && enhanced.children.length > 0) {
|
|
|
+ enhanced.children = enhanceDeptNodes(enhanced.children);
|
|
|
+ }
|
|
|
+ return enhanced;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
/** 加载部门树数据 */
|
|
|
const loadDeptTree = async () => {
|
|
|
try {
|
|
|
loading.value = true;
|
|
|
+ // 先加载班级状态缓存
|
|
|
+ await ensureClassStatusCache();
|
|
|
+
|
|
|
const response = await request({
|
|
|
url: '/system/user/deptTree',
|
|
|
method: 'get',
|
|
|
@@ -460,7 +623,45 @@ const loadDeptTree = async () => {
|
|
|
}
|
|
|
});
|
|
|
if (response && response.code === 200) {
|
|
|
- deptTreeData.value = response.data || [];
|
|
|
+ const rawData = response.data || [];
|
|
|
+ console.log(`[部门树] 加载原始数据,共 ${rawData.length} 个根节点`);
|
|
|
+
|
|
|
+ // 调试:收集所有叶子节点(可能是班级节点)
|
|
|
+ function collectLeafNodes(nodes: any[], path = '', depth = 0): any[] {
|
|
|
+ const results: any[] = [];
|
|
|
+ nodes.forEach((node) => {
|
|
|
+ const currentPath = path ? `${path} > ${node.label}` : node.label;
|
|
|
+ const isLeaf = !node.children || node.children.length === 0;
|
|
|
+
|
|
|
+ if (isLeaf) {
|
|
|
+ results.push({
|
|
|
+ path: currentPath,
|
|
|
+ depth: depth,
|
|
|
+ id: node.id,
|
|
|
+ label: node.label,
|
|
|
+ deptCategory: node.deptCategory,
|
|
|
+ dept_category: node.dept_category,
|
|
|
+ params: node.params
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ results.push(...collectLeafNodes(node.children, currentPath, depth + 1));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+
|
|
|
+ const leafNodes = collectLeafNodes(rawData);
|
|
|
+ console.log(`[部门树] 找到 ${leafNodes.length} 个叶子节点(可能是班级)`);
|
|
|
+ console.log('[部门树] 前10个叶子节点示例:', leafNodes.slice(0, 10));
|
|
|
+
|
|
|
+ // 增强节点数据(添加班级状态)
|
|
|
+ const enhancedTree = enhanceDeptNodes(rawData);
|
|
|
+ // 保存原始树数据
|
|
|
+ originalTreeData.value = enhancedTree;
|
|
|
+ deptTreeData.value = enhancedTree;
|
|
|
+ console.log('[部门树] 节点增强完成');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('获取部门树数据失败:', error);
|
|
|
@@ -544,12 +745,69 @@ const handleScopeDialogClose = () => {
|
|
|
// 清空相关状态
|
|
|
currentBatch.value = null;
|
|
|
checkedDeptIds.value = [];
|
|
|
+ // 重置过滤状态为"全部"
|
|
|
+ classStatusFilter.value = 'all';
|
|
|
// 清空树形组件的勾选状态
|
|
|
if (scopeTreeRef.value) {
|
|
|
scopeTreeRef.value.setCheckedKeys([]);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+/** 过滤后的树数据(计算属性) */
|
|
|
+const filteredTreeData = computed(() => {
|
|
|
+ if (classStatusFilter.value === 'all') {
|
|
|
+ return originalTreeData.value;
|
|
|
+ } else if (classStatusFilter.value === 'normal') {
|
|
|
+ // 只显示状态为"1"(正常)的班级节点及其父节点
|
|
|
+ return filterTreeByClassStatus(originalTreeData.value, '1');
|
|
|
+ }
|
|
|
+ return originalTreeData.value;
|
|
|
+});
|
|
|
+
|
|
|
+/** 根据班级状态过滤树数据 */
|
|
|
+const filterTreeByClassStatus = (nodes: any[], targetStatus: string): any[] => {
|
|
|
+ if (!nodes || nodes.length === 0) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return nodes
|
|
|
+ .map((node) => {
|
|
|
+ const isClass = isClassNode(node);
|
|
|
+
|
|
|
+ // 如果是班级节点,检查状态
|
|
|
+ if (isClass) {
|
|
|
+ const nodeStatus = node.classStatus;
|
|
|
+ // 如果状态匹配,保留该节点
|
|
|
+ if (nodeStatus === targetStatus) {
|
|
|
+ return { ...node };
|
|
|
+ }
|
|
|
+ // 状态不匹配,返回null(将被过滤掉)
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 非班级节点,递归处理子节点
|
|
|
+ const filteredChildren = node.children && node.children.length > 0 ? filterTreeByClassStatus(node.children, targetStatus) : [];
|
|
|
+
|
|
|
+ // 如果有子节点被保留,则保留当前节点
|
|
|
+ if (filteredChildren.length > 0) {
|
|
|
+ return {
|
|
|
+ ...node,
|
|
|
+ children: filteredChildren
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 没有符合条件的子节点,过滤掉当前节点
|
|
|
+ return null;
|
|
|
+ })
|
|
|
+ .filter((node) => node !== null);
|
|
|
+};
|
|
|
+
|
|
|
+/** 处理过滤条件变化 */
|
|
|
+const handleFilterChange = () => {
|
|
|
+ // 过滤逻辑由计算属性 filteredTreeData 自动处理
|
|
|
+ // 这里可以添加额外的逻辑,比如重置展开状态等
|
|
|
+};
|
|
|
+
|
|
|
/** 查看详细按钮操作 */
|
|
|
const handleView = async (row: StuColBatchVO) => {
|
|
|
try {
|
|
|
@@ -609,4 +867,29 @@ onMounted(() => {
|
|
|
.mb-3 {
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
+
|
|
|
+.scope-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-buttons {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.scope-node {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.scope-label {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.scope-tag :deep(.el-tag) {
|
|
|
+ margin-left: 0;
|
|
|
+}
|
|
|
</style>
|