zlwu 11 月之前
父節點
當前提交
4c8434595a

+ 3 - 3
backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/studentMsg/XgStudentMsgMapper.xml

@@ -205,7 +205,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           AND sm.major_id IS NOT NULL
           AND sm.class_id IS NOT NULL
         GROUP BY c.grade, sm.college_id, sm.college_name, sm.major_id, sm.major_name, sm.class_id, sm.class_name
-        ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name, sm.create_time DESC
+        ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name, MAX(sm.create_time) DESC
     </select>
 
     <!-- 查询二级学院下的学院、专业、班级学生统计信息(按床位分配统计) -->
@@ -255,7 +255,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           AND sm.major_id IS NOT NULL
           AND sm.class_id IS NOT NULL
         GROUP BY c.grade, sm.college_id, sm.college_name, sm.major_id, sm.major_name, sm.class_id, sm.class_name
-        ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name, sm.create_time DESC
+        ORDER BY c.grade DESC, sm.college_name, sm.major_name, sm.class_name, MAX(sm.create_time) DESC
     </select>
 
     <!-- 查询二级学院下的学院床位分配统计信息(基于学生表统计需求,基于床位分配表统计已分配) -->
@@ -301,7 +301,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           AND (d.parent_id = #{secondaryCollegeId} OR d.dept_id = #{secondaryCollegeId} OR FIND_IN_SET(#{secondaryCollegeId}, d.ancestors))
           AND sm.college_id IS NOT NULL
         GROUP BY sm.college_id, sm.college_name
-        ORDER BY sm.college_name, sm.create_time DESC
+        ORDER BY sm.college_name, MAX(sm.create_time) DESC
     </select>
 
 </mapper>

+ 3 - 3
web/src/views/jcbt/poverty/Application/index.vue

@@ -536,7 +536,7 @@
 </template>
 
 <script setup name="JcbtPovertyApplication">
-import { getCurrentInstance, ref, reactive, onMounted, nextTick, computed } from 'vue';
+import { getCurrentInstance, ref, reactive, onMounted, nextTick, computed, toRefs } from 'vue';
 import { getToken } from '@/utils/auth';
 import {
   listPovertyApplication,
@@ -563,7 +563,7 @@ import { ElMessageBox, ElMessage } from 'element-plus';
 const { proxy } = getCurrentInstance();
 
 // 字典:资助类型、家庭经济情况
-const { jcbt_assistance_type, jcbt_family_economy_status, jcbt_academic_year, jcbt_status } = toRefs<any>(
+const { jcbt_assistance_type, jcbt_family_economy_status, jcbt_academic_year, jcbt_status } = toRefs(
   proxy?.useDict('jcbt_assistance_type', 'jcbt_family_economy_status', 'jcbt_academic_year', 'jcbt_status') || {}
 );
 
@@ -805,7 +805,7 @@ function handleRecognitionTypeChange(value) {
 
 /** 处理申请资助类型选择变化 */
 function handleAssistanceTypeChange(value) {
-  const selected = (jcbt_assistance_type?.value || []).find((item: any) => item.value === value);
+  const selected = (jcbt_assistance_type?.value || []).find((item) => item.value === value);
   form.value.assistanceTypeNames = selected ? selected.label : null;
 }
 

+ 286 - 3
web/src/views/xg/xsjcxx/XgStuColBatch/stuColBatch/index.vue

@@ -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>