This commit is contained in:
zlwu committed 2025-11-13 18:07:07 +08:00
1 parent 6ca85a1b15
commit 7d4bfaecb3
45 files changed
+7015 -1762

No files matched your search

@@ -37,6 +37,24 @@ export function updateClassMsg(data: ClassMsgForm) {
});
}
// 仅更新班级状态
export function updateClassStatus(data: { id: string | number; classStatus: string | number }) {
return request({
url: '/classMsg/classMsg/status',
method: 'put',
data
});
}
// 批量仅更新班级状态
export function updateClassStatusBatch(data: { ids: Array<string | number>; classStatus: string | number }) {
return request({
url: '/classMsg/classMsg/status/batch',
method: 'put',
data
});
}
// 删除班级信息-新版
export function delClassMsg(id: string | number) {
return request({
@@ -8,6 +8,7 @@ export interface XgDeptTreeVO extends BaseEntity {
weight: number;
children: XgDeptTreeVO[];
disabled: boolean;
deptCategory?: string; // 部门类别编码(如 'bj-xxx' 表示班级)
}
// 学生查询参数
@@ -93,3 +93,15 @@ export const listActiveClassTeachers = (params: { phone?: string; name?: string
params
});
};
/**
* 获取全部班主任信息(按班级状态排序)
* @param params 包含 phone 和 name 字段
*/
export const listAllClassTeachers = (params: { phone?: string; name?: string }) => {
return request({
url: '/xgStudentManager/studentManager/listAllClassTeachers',
method: 'get',
params
});
};
@@ -59,6 +59,27 @@
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="CircleCheck"
:disabled="multiple"
@click="handleBatchGraduate"
v-hasPermi="['classMsg:classMsg:edit']"
>毕业</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="VideoPause" :disabled="multiple" @click="handleBatchPause" v-hasPermi="['classMsg:classMsg:edit']"
>暂停</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="CloseBold" :disabled="multiple" @click="handleBatchDissolve" v-hasPermi="['classMsg:classMsg:edit']"
>解散</el-button
>
</el-col>
<!-- <el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['classMsg:classMsg:export']">导出</el-button>
</el-col> -->
@@ -286,7 +307,7 @@
<el-form-item label="班级规章制度" prop="classRules" v-if="false">
<el-input v-model="form.classRules" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="班级状态" prop="classStatus" v-if="false">
<el-form-item label="班级状态" prop="classStatus">
<el-select v-model="form.classStatus" placeholder="请选择班级状态" style="width: 100%">
<!-- <el-option label="正常" :value="1" />
<el-option label="暂停" :value="2" />
@@ -413,7 +434,15 @@
</template>
<script setup name="ClassMsg" lang="ts">
import { listClassMsg, getClassMsg, delClassMsg, addClassMsg, updateClassMsg } from '@/api/xg/xsjcxx/classmsg/classMsg';
import {
listClassMsg,
getClassMsg,
delClassMsg,
addClassMsg,
updateClassMsg,
updateClassStatus,
updateClassStatusBatch
} from '@/api/xg/xsjcxx/classmsg/classMsg';
import { listColleges, collegeTree, listMajorsByCollegeId } from '@/api/xg/xsjcxx/classmsg/college';
import { ClassMsgVO, ClassMsgQuery, ClassMsgForm, SysDeptVo } from '@/api/xg/xsjcxx/classmsg/types';
import { ComponentInternalInstance, ref, reactive, toRefs, onMounted, getCurrentInstance, watch } from 'vue';
@@ -528,7 +557,7 @@ const initFormData: ClassMsgForm = {
classMonitorName: undefined,
deputyMonitorId: undefined,
deputyMonitorName: undefined,
classStatus: 1,
classStatus: '1',
isGraduation: '0',
graduationDate: undefined,
plannedCapacity: undefined,
@@ -575,6 +604,7 @@ const data = reactive<PageData<ClassMsgForm, ClassMsgQuery>>({
enrollmentYear: [{ required: true, message: '入学年份不能为空', trigger: 'blur' }],
majorId: [{ required: true, message: '专业不能为空', trigger: 'blur' }],
classType: [{ required: true, message: '学历层级不能为空', trigger: 'blur' }],
classStatus: [{ required: true, message: '班级状态不能为空', trigger: 'change' }],
schoolSystem: [{ required: true, message: '学制(年)不能为空', trigger: 'blur' }],
graduationYear: [{ required: true, message: '毕业年份不能为空', trigger: 'blur' }],
collegeId: [{ required: true, message: '学院不能为空', trigger: 'blur' }]
@@ -587,7 +617,7 @@ const { queryParams, form, rules } = toRefs(data);
const getList = async () => {
loading.value = true;
const res = await listClassMsg(queryParams.value);
classMsgList.value = res.rows;
classMsgList.value = res.rows || [];
total.value = res.total;
loading.value = false;
};
@@ -722,14 +752,25 @@ const handleUpdate = async (row?: ClassMsgVO) => {
console.log(res);
Object.assign(form.value, res.data);
// 确保classType是数字类型
if (form.value.classType !== undefined) {
// 确保classType是字符串类型,null时设置为undefined
if (form.value.classType !== undefined && form.value.classType !== null) {
form.value.classType = String(form.value.classType);
} else {
form.value.classType = undefined;
}
// 确保isGraduation是字符串类型
if (form.value.isGraduation !== undefined) {
// 确保isGraduation是字符串类型,null时设置为undefined
if (form.value.isGraduation !== undefined && form.value.isGraduation !== null) {
form.value.isGraduation = String(form.value.isGraduation);
} else {
form.value.isGraduation = undefined;
}
// 确保classStatus是字符串类型,null时设置为undefined
if (form.value.classStatus !== undefined && form.value.classStatus !== null) {
form.value.classStatus = String(form.value.classStatus);
} else {
form.value.classStatus = undefined;
}
// 获取该学院下的专业列表
@@ -780,6 +821,55 @@ const handleDelete = async (row?: ClassMsgVO) => {
await getList();
};
/** 批量状态更新通用方法 */
const handleBatchChangeStatus = async (status: '2' | '3' | '4', actionLabel: string) => {
// 选中名称用于确认提示
const classNames =
classMsgList.value
.filter((item) => ids.value.includes(item.id))
.map((item) => item.className)
.join('、') || '';
await proxy?.$modal.confirm(`是否将班级「${classNames}」批量设置为【${actionLabel}】?`).finally(() => (loading.value = false));
buttonLoading.value = true;
try {
// 批量更新所选班级状态(后端单次处理)
await updateClassStatusBatch({ ids: ids.value, classStatus: status });
proxy?.$modal.msgSuccess(`${actionLabel}操作成功`);
await getList();
} finally {
buttonLoading.value = false;
}
};
/** 批量设置为毕业 */
const handleBatchGraduate = async () => {
if (!ids.value || ids.value.length === 0) {
proxy?.$modal.msgWarning('请先勾选需要操作的班级');
return;
}
await handleBatchChangeStatus('3', '毕业');
};
/** 批量设置为暂停 */
const handleBatchPause = async () => {
if (!ids.value || ids.value.length === 0) {
proxy?.$modal.msgWarning('请先勾选需要操作的班级');
return;
}
await handleBatchChangeStatus('2', '暂停');
};
/** 批量设置为解散 */
const handleBatchDissolve = async () => {
if (!ids.value || ids.value.length === 0) {
proxy?.$modal.msgWarning('请先勾选需要操作的班级');
return;
}
await handleBatchChangeStatus('4', '解散');
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
@@ -1,14 +1,21 @@
<template>
<el-dialog :title="getScopeDialogTitle(type)" v-model="visible" width="600px" append-to-body destroy-on-close>
<el-form :model="form" label-width="100px">
<el-form-item :label="getScopeName(type)">
<el-form-item :label="getScopeName(type)" class="scope-form-item">
<div v-if="loading" class="loading-container">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
<div v-else-if="type === 'custom'">
<!-- 过滤按钮区域 -->
<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
:data="scopeOptions.custom"
:data="filteredTreeData"
show-checkbox
node-key="id"
:default-checked-keys="form.selectedScopes"
@@ -25,12 +32,36 @@
padding: 10px;
box-sizing: border-box;
"
/>
>
<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 }}|{{ data.classStatus || '无状态' }}]
</span> -->
</span>
</template>
</el-tree>
</div>
<div v-else-if="type === 'teacher'">
<el-checkbox-group v-model="form.selectedScopes">
<el-checkbox v-for="item in scopeOptions.teacher" :key="item.id" :label="item.id">
{{ item.label }}
<span class="scope-checkbox-label">
<span>{{ item.label }}</span>
<dict-tag
v-if="isClassNode(item) && item.classStatus !== undefined && item.classStatus !== null"
:options="classStatusOptions"
:value="item.classStatus"
class="scope-tag"
/>
</span>
</el-checkbox>
</el-checkbox-group>
</div>
@@ -46,11 +77,12 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue';
import { ref, reactive, computed, watch, getCurrentInstance, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import axios from 'axios';
import request from '@/utils/request';
import { listClassMsg } from '@/api/xg/xsjcxx/classMsg/classMsg';
import type { ClassMsgQuery, ClassMsgVO } from '@/api/xg/xsjcxx/classMsg/types';
const props = defineProps({
modelValue: {
@@ -69,6 +101,22 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue', 'submit']);
const { proxy } = getCurrentInstance() as any;
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 classStatusCache = new Map<string, string>();
let classStatusLoaded = false;
// 计算属性:对话框可见性
const visible = computed({
get: () => props.modelValue,
@@ -81,6 +129,12 @@ const scopeOptions = reactive({
teacher: [] as any[]
});
// 原始树数据(未过滤)
const originalTreeData = ref<any[]>([]);
// 班级状态过滤:'all' 全部, 'normal' 正常
const classStatusFilter = ref<'all' | 'normal'>('all');
// 加载状态
const loading = ref(false);
@@ -101,12 +155,127 @@ async function fetchDeptTree() {
}
});
if (response && response.code === 200) {
// custom类型直接使用树形结构
const rawData = response.data || [];
// 调试:递归收集所有叶子节点(可能是班级节点)
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) {
// 收集所有字段,看看deptCategory在哪里
results.push({
path: currentPath,
depth: depth,
id: node.id,
label: node.label,
// 检查所有可能的字段位置
deptCategory: node.deptCategory,
dept_category: node.dept_category,
params: node.params,
// 打印节点的所有键,看看还有什么字段
allKeys: Object.keys(node),
// 打印完整节点(限制深度避免过多输出)
fullNode: depth >= 3 ? node : undefined
});
}
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));
// 调试:查找所有可能的班级节点(检查所有字段和位置)
function findClassNodes(nodes: any[], path = ''): any[] {
const results: any[] = [];
nodes.forEach((node) => {
const currentPath = path ? `${path} > ${node.label}` : node.label;
// 检查所有可能的字段位置
const category =
node.deptCategory ?? node.dept_category ?? node.params?.deptCategory ?? node.data?.deptCategory ?? node.data?.dept_category;
// 检查是否是班级节点(多种可能的格式)
const isClass =
category &&
typeof category === 'string' &&
(category.toLowerCase().startsWith('bj-') || category.toLowerCase().includes('class') || category.toLowerCase().includes('班级'));
if (isClass) {
results.push({
path: currentPath,
id: node.id,
label: node.label,
category: category,
deptCategory: node.deptCategory,
dept_category: node.dept_category,
params: node.params,
data: node.data
});
}
if (node.children && node.children.length > 0) {
results.push(...findClassNodes(node.children, currentPath));
}
});
return results;
}
const foundClassNodes = findClassNodes(rawData);
console.log(`[班级节点查找] 找到 ${foundClassNodes.length} 个可能的班级节点:`, foundClassNodes.slice(0, 10));
// 如果没找到,尝试通过节点名称模式识别(如"西餐运营251"这种格式)
if (foundClassNodes.length === 0) {
console.log('[调试] 未通过deptCategory找到班级节点,尝试通过节点名称模式识别...');
const namePatternNodes = leafNodes.filter((node) => {
// 匹配类似"西餐运营251"、"中烹252"这种格式(名称+数字)
const nameMatch = /^.+?\d{3}$/.test(node.label);
return nameMatch;
});
console.log(`[名称模式匹配] 找到 ${namePatternNodes.length} 个符合"名称+3位数字"格式的节点:`, namePatternNodes.slice(0, 10));
}
// 先加载班级状态缓存
await ensureClassStatusCache();
// 增强节点数据(添加班级状态)
const enhancedTree = enhanceDeptNodes(rawData);
// 调试:统计增强后的班级节点数量
let classNodeCount = 0;
let classNodeWithStatusCount = 0;
function countClassNodes(nodes: any[]) {
nodes.forEach((node) => {
// 使用 isClassNode 函数判断(支持两种方式:deptCategory字段 或 名称模式)
if (isClassNode(node)) {
classNodeCount++;
if (node.classStatus) {
classNodeWithStatusCount++;
}
}
if (node.children && node.children.length > 0) {
countClassNodes(node.children);
}
});
}
countClassNodes(enhancedTree);
console.log(`[班级状态统计] 总班级节点: ${classNodeCount}, 有状态的节点: ${classNodeWithStatusCount}`);
// 保存原始树数据
originalTreeData.value = enhancedTree;
if (props.type === 'custom') {
scopeOptions.custom = response.data || [];
scopeOptions.custom = enhancedTree;
} else if (props.type === 'teacher') {
// teacher类型仍可用扁平化
scopeOptions.teacher = flattenTreeData(response.data || []);
scopeOptions.teacher = flattenTreeData(enhancedTree);
} else {
scopeOptions.custom = enhancedTree;
}
}
} catch (error) {
@@ -125,7 +294,9 @@ function flattenTreeData(treeData: any[]): any[] {
nodes.forEach((node) => {
result.push({
id: node.id,
label: node.label
label: node.label,
deptCategory: node.deptCategory,
classStatus: node.classStatus
});
if (node.children && node.children.length > 0) {
@@ -138,6 +309,130 @@ function flattenTreeData(treeData: any[]): any[] {
return result;
}
async function ensureClassStatusCache() {
if (classStatusLoaded) {
return;
}
try {
const params: ClassMsgQuery = {
pageNum: 1,
pageSize: 9999,
params: {}
};
const res = await listClassMsg(params);
const rows: ClassMsgVO[] = res?.rows || [];
// 建立缓存映射:sys_dept.dept_id = xg_class_msg.id -> class_status
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);
}
});
classStatusLoaded = true;
console.log(`[班级状态缓存] 加载完成,共 ${classStatusCache.size} 条记录`);
// 调试:打印前5条缓存记录
if (classStatusCache.size > 0) {
const sampleKeys = Array.from(classStatusCache.keys()).slice(0, 5);
console.log(
'[班级状态缓存] 示例记录:',
sampleKeys.map((key) => ({ key, status: classStatusCache.get(key) }))
);
}
} catch (error) {
console.error('加载班级状态失败:', error);
ElMessage.warning('加载班级状态失败,班级状态标签可能无法显示');
}
}
function 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;
}
// 判断是否是班级节点(支持两种方式:deptCategory字段 或 名称模式)
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;
});
}
function resolveClassStatus(node: any): string | undefined {
// 判断是否是班级节点
// 方法1:通过 deptCategory 字段判断(优先)
const category = node.deptCategory ?? node.dept_category ?? node.params?.deptCategory;
const isClassByCategory = category && typeof category === 'string' && category.toLowerCase().startsWith('bj-');
// 方法2:通过名称模式判断(备用方案)
// 如果节点是叶子节点(没有children或children为空),且名称符合"名称+3位数字"格式,则认为是班级节点
const isLeaf = !node?.children || node.children.length === 0;
const isClassByName = isLeaf && node?.label && typeof node.label === 'string' && /^.+?\d{3}$/.test(node.label);
// 如果不是班级节点,直接返回
if (!isClassByCategory && !isClassByName) {
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)) {
return classStatusCache.get(key);
}
return undefined;
}
function 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;
}
// 新增:获取历史范围数据
async function fetchSelectedScopes(managerId: number | string) {
if (!managerId) return;
@@ -160,6 +455,8 @@ async function fetchSelectedScopes(managerId: number | string) {
watch(visible, async (newVal) => {
if (newVal) {
const managerId = props.admin?.managerId || props.admin?.id;
// 重置过滤状态为"全部"
classStatusFilter.value = 'all';
await fetchDeptTree();
await fetchSelectedScopes(managerId);
} else {
@@ -236,6 +533,63 @@ function cancel() {
// 从props获取类型的响应式引用
const type = computed(() => props.type);
// 过滤后的树数据(计算属性)
const filteredTreeData = computed(() => {
if (classStatusFilter.value === 'all') {
return scopeOptions.custom;
} else if (classStatusFilter.value === 'normal') {
// 只显示状态为"1"(正常)的班级节点及其父节点
return filterTreeByClassStatus(scopeOptions.custom, '1');
}
return scopeOptions.custom;
});
// 根据班级状态过滤树数据
function 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);
}
// 处理过滤条件变化
function handleFilterChange() {
// 过滤逻辑由计算属性 filteredTreeData 自动处理
// 这里可以添加额外的逻辑,比如重置展开状态等
}
// 暴露组件以修复导出问题
defineExpose({
submitForm,
@@ -245,6 +599,19 @@ defineExpose({
</script>
<style scoped>
.scope-node,
.scope-checkbox-label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.scope-label {
display: inline-flex;
align-items: center;
}
.scope-tag :deep(.el-tag) {
margin-left: 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
@@ -268,4 +635,21 @@ defineExpose({
margin-right: 8px;
font-size: 16px;
}
.scope-form-item {
display: flex;
align-items: flex-start;
}
.scope-form-item :deep(.el-form-item__label) {
display: flex;
align-items: center;
padding-top: 8px; /* 与 filter-buttons 的 padding-top 对齐 */
margin-top: 0;
height: auto;
line-height: 1.5;
}
.scope-form-item :deep(.el-form-item__content) {
display: flex;
align-items: flex-start;
flex-direction: column;
}
</style>
@@ -154,6 +154,11 @@
</template>
</el-table-column>
<el-table-column prop="className" label="所管理班级" align="center" />
<el-table-column prop="classStatus" label="班级状态" align="center">
<template #default="scope">
<dict-tag :options="xg_class_status" :value="scope.row.classStatus" />
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" align="center" />
<!-- <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
<template #default="scope">
@@ -179,11 +184,12 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue';
import { ref, reactive, onMounted, watch, toRefs, getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import AdminDialog from './components/AdminDialog.vue';
import ScopeDialog from './components/ScopeDialog.vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { listStudentManager, delStudentManager, listActiveClassTeachers } from '@/api/xg/xsjcxx/xgStudentManager/studentManager';
import { listStudentManager, delStudentManager, listAllClassTeachers } from '@/api/xg/xsjcxx/xgStudentManager/studentManager';
import type { StudentManagerQuery } from '@/api/xg/xsjcxx/xgStudentManager/studentManager/types';
import service from '@/utils/request';
@@ -220,6 +226,9 @@ const currentAdmin = ref<any>(null);
const scopeDialogVisible = ref(false);
const scopeType = ref('');
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { xg_class_status } = toRefs<any>(proxy?.useDict('xg_class_status') || {});
// 班主任模拟数据 (预留,后续完善)
const mockTeacherData = [
{ id: 5, name: '刘老师', phone: '13800138005', sex: 0, scope: '高一(1)班', remark: '数学教师' },
@@ -250,7 +259,7 @@ async function fetchAdmins(type: string) {
phone: searchForm.phone,
name: searchForm.managerName // teacherSearchForm 实际只有 managerName 字段
};
const res = await listActiveClassTeachers(params);
const res = await listAllClassTeachers(params);
teacherAdmins.value = res.data || [];
}
} catch (error) {
@@ -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">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="normal">正常</el-radio-button>
</el-radio-group>
</div>
<el-tree
ref="deptTreeRef"
class="mt-2 dept-tree"
@@ -16,7 +23,19 @@
highlight-current
default-expand-all
@node-click="handleNodeClick"
/>
>
<template #default="{ data }">
<span class="dept-tree-node">
<span class="dept-tree-node__label">{{ data.label }}</span>
<dict-tag
v-if="isClassNode(data) && hasClassStatus(data)"
:options="xg_class_status || []"
:value="data.classStatus"
class="dept-tree-node__tag"
/>
</span>
</template>
</el-tree>
</el-card>
</div>
@@ -431,10 +450,12 @@ import {
dismissCadre
} from '@/api/xg/xsjcxx/xgClassPosMsg/xgClassPosMsg';
import { XgClassPosMsgVO, XgClassPosMsgQuery, XgClassPosMsgForm } from '@/api/xg/xsjcxx/xgClassPosMsg/xgClassPosMsg/types';
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, type FormInstance } from 'element-plus';
import { XgDeptTreeSelect, deptTreeSelect } from '@/api/xg/xsjcxx/studentMsg/index';
import request from '@/utils/request';
import { listClassMsg } from '@/api/xg/xsjcxx/classMsg/classMsg';
import type { ClassMsgQuery, ClassMsgVO } from '@/api/xg/xsjcxx/classMsg/types';
interface ComponentInternalInstance {
proxy?: any;
}
@@ -451,8 +472,8 @@ interface PageData<T, Q> {
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { stu_appointment_method, sys_yesno, stu_cadre_status } = toRefs<any>(
proxy?.useDict('stu_appointment_method', 'sys_yesno', 'stu_cadre_status')
const { stu_appointment_method, sys_yesno, stu_cadre_status, xg_class_status } = toRefs<any>(
proxy?.useDict('stu_appointment_method', 'sys_yesno', 'stu_cadre_status', 'xg_class_status') || {}
);
const xgClassPosMsgList = ref<XgClassPosMsgVO[]>([]);
@@ -467,8 +488,10 @@ const total = ref(0);
// 部门树相关变量
const deptTreeRef = ref();
const deptOptions = ref<any[]>([]);
const rawDeptTree = ref<any[]>([]);
const deptName = ref('');
const currentSelectedNode = ref<any>(null);
const classStatusFilter = ref<'all' | 'normal'>('all');
// 学生选择相关变量
const studentOptions = ref<any[]>([]);
@@ -478,6 +501,35 @@ const studentSearchLoading = ref(false);
const positionOptions = ref<any[]>([]);
const positionSearchLoading = ref(false);
const classStatusMap = new Map<string, string>();
let classStatusLoaded = false;
let classStatusLoading = false;
const classStatusOrderMap = computed(() => {
const map = new Map<string, number>();
const options = xg_class_status?.value || [];
options.forEach((item: any, index: number) => {
if (item?.value !== undefined && item?.value !== null) {
map.set(String(item.value), index);
}
});
return map;
});
const compareLabels = (a: string | undefined, b: string | undefined) => {
const labelA = a || '';
const labelB = b || '';
return labelA.localeCompare(labelB, 'zh-Hans-CN-u-co-pinyin');
};
const getClassStatusOrder = (status: string | undefined) => {
if (status === undefined || status === null || status === '') {
return Number.MAX_SAFE_INTEGER;
}
const order = classStatusOrderMap.value.get(String(status));
return order !== undefined ? order : Number.MAX_SAFE_INTEGER;
};
const queryFormRef = ref<FormInstance>();
const xgClassPosMsgFormRef = ref<FormInstance>();
@@ -617,6 +669,152 @@ watchEffect(
{ flush: 'post' }
);
/** 班级节点是否有状态 */
const hasClassStatus = (node: any) => {
const status = node?.classStatus ?? node?.class_status;
return status !== undefined && status !== null && status !== '';
};
/** 获取节点的deptCategory */
const getDeptCategory = (node: any): string | undefined => {
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 sortDeptTreeNodes = (nodes: any[]): any[] => {
if (!Array.isArray(nodes)) {
return [];
}
const clonedNodes = nodes.map((node) => {
const cloned: any = { ...node };
if (Array.isArray(cloned.children)) {
cloned.children = sortDeptTreeNodes(cloned.children);
}
return cloned;
});
clonedNodes.sort((a, b) => {
const aIsClass = isClassNode(a);
const bIsClass = isClassNode(b);
if (aIsClass && bIsClass) {
const statusCompare = getClassStatusOrder(a.classStatus ?? resolveClassStatus(a)) - getClassStatusOrder(b.classStatus ?? resolveClassStatus(b));
if (statusCompare !== 0) {
return statusCompare;
}
}
return compareLabels(a.label, b.label);
});
return clonedNodes;
};
/** 增强部门树节点,补充班级状态 */
const enhanceDeptTreeNodes = (nodes: any[]): any[] => {
return (nodes || []).map((node) => {
const enhanced: any = { ...node };
const category = getDeptCategory(enhanced);
if (category && !enhanced.deptCategory) {
enhanced.deptCategory = category;
}
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 extractDeptTree = (payload: any): any[] => {
if (Array.isArray(payload)) {
return payload;
}
if (payload?.data && Array.isArray(payload.data)) {
return payload.data;
}
if (payload?.data?.data && Array.isArray(payload.data.data)) {
return payload.data.data;
}
return [];
};
/** 处理并设置部门树 */
const processAndSetDeptTree = async (treeData: any[]) => {
try {
await ensureClassStatusMap();
} catch (error) {
console.warn('加载班级状态时发生异常,继续渲染部门树', error);
}
const processedTree = enhanceDeptTreeNodes(treeData);
const sortedTree = sortDeptTreeNodes(processedTree);
rawDeptTree.value = sortedTree;
applyDeptFilter(true);
};
/** 筛选节点方法 */
const filterNode = (value: string, data: any) => {
if (!value) return true;
@@ -627,29 +825,125 @@ const filterNode = (value: string, data: any) => {
const getDeptTree = async () => {
try {
const res = await XgDeptTreeSelect(101);
// XgDeptTreeSelect返回的数据结构与deptTreeSelect不同,需要进行适当处理
if (res.data && Array.isArray(res.data)) {
deptOptions.value = res.data;
// 设置根节点为默认选中节点
setDefaultRootNode(res.data);
const treeData = extractDeptTree(res);
if (treeData.length > 0) {
await processAndSetDeptTree(treeData);
} else {
console.warn('部门树数据格式不符合预期');
// 回退到默认处理
const backup = await deptTreeSelect();
deptOptions.value = backup.data.data;
// 设置根节点为默认选中节点
setDefaultRootNode(backup.data.data);
const backupTree = extractDeptTree(backup);
await processAndSetDeptTree(backupTree);
}
} catch (error) {
console.error('获取部门树失败:', error);
// 如果API调用失败,可以使用默认的deptTreeSelect作为备选
const backup = await deptTreeSelect();
deptOptions.value = backup.data.data;
// 设置根节点为默认选中节点
setDefaultRootNode(backup.data.data);
const backupTree = extractDeptTree(backup);
await processAndSetDeptTree(backupTree);
}
};
const findNodeById = (nodes: any[], id: number | string): any | null => {
if (!Array.isArray(nodes)) {
return null;
}
for (const node of nodes) {
if (node?.id === id) {
return node;
}
if (Array.isArray(node?.children)) {
const child = findNodeById(node.children, id);
if (child) {
return child;
}
}
}
return null;
};
const filterTreeByClassStatus = (nodes: any[], targetStatus: string | number | undefined): any[] => {
if (!Array.isArray(nodes) || targetStatus === undefined) {
return nodes ? JSON.parse(JSON.stringify(nodes)) : [];
}
const result: any[] = [];
nodes.forEach((node) => {
const clonedNode = { ...node };
if (Array.isArray(clonedNode.children)) {
clonedNode.children = filterTreeByClassStatus(clonedNode.children, targetStatus);
}
if (isClassNode(clonedNode)) {
const status = resolveClassStatus(clonedNode);
if (status === String(targetStatus)) {
result.push(clonedNode);
}
} else if (Array.isArray(clonedNode.children) && clonedNode.children.length > 0) {
result.push(clonedNode);
}
});
return sortDeptTreeNodes(result);
};
const normalClassStatusValue = computed(() => {
const options = xg_class_status?.value || [];
const normalOption =
options.find((item: any) => item?.label === '正常') ||
options.find((item: any) => String(item?.value).toLowerCase() === 'normal') ||
options.find((item: any) => String(item?.value) === '1');
return normalOption ? normalOption.value : options[0]?.value;
});
const reselectOrResetNode = () => {
const currentId = currentSelectedNode.value?.id;
if (!currentId) {
return;
}
const matched = findNodeById(deptOptions.value, currentId);
if (matched) {
currentSelectedNode.value = matched;
queryParams.value.deptId = matched.id;
nextTick(() => {
deptTreeRef.value?.setCurrentKey(matched.id);
});
} else {
currentSelectedNode.value = null;
queryParams.value.deptId = undefined;
setDefaultRootNode(deptOptions.value);
}
};
const applyDeptFilter = (resetSelection = false) => {
const sourceTree = rawDeptTree.value || [];
if (classStatusFilter.value === 'normal') {
const targetStatus = normalClassStatusValue.value;
if (targetStatus === undefined) {
deptOptions.value = JSON.parse(JSON.stringify(sourceTree));
} else {
deptOptions.value = filterTreeByClassStatus(sourceTree, targetStatus);
}
} else {
deptOptions.value = JSON.parse(JSON.stringify(sourceTree));
}
if (resetSelection) {
setDefaultRootNode(deptOptions.value);
} else {
reselectOrResetNode();
}
};
watch(classStatusFilter, () => {
applyDeptFilter(false);
});
watch(
() => xg_class_status?.value,
() => {
if (classStatusFilter.value === 'normal') {
applyDeptFilter(false);
}
}
);
/** 设置默认根节点并触发查询 */
const setDefaultRootNode = (treeData: any[]) => {
if (treeData && treeData.length > 0) {
@@ -1074,11 +1368,24 @@ onMounted(() => {
.dept-tree {
height: 600px;
overflow-y: auto;
overflow-x: auto;
overflow-x: hidden;
width: 330px; /* 固定宽度,超出时显示滚动条 */
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;
}
/* 强制覆盖el-tree的默认样式 */
.dept-tree :deep(.el-tree-node) {
white-space: nowrap !important;
@@ -1124,6 +1431,16 @@ onMounted(() => {
width: 100% !important;
}
.dept-tree-node {
display: inline-flex;
align-items: center;
gap: 6px;
}
.dept-tree-node__tag :deep(.el-tag) {
margin-left: 0;
}
/* 右侧面板 - 占据剩余空间 */
.right-panel {
flex: 1;