Files
hzdyjs/web/src/views/xg/xsjcxx/classmsg/majormsg/index.vue
T

639 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="p-2 page-container">
<div class="flex h-full">
<!-- 左侧学院树 -->
<div class="w-80 mr-4">
<el-card shadow="never" class="h-full">
<template #header>
<div class="flex items-center">
<span class="text-lg font-medium">二级学院</span>
</div>
</template>
<el-tree
ref="collegeTreeRef"
:data="collegeTreeData"
:props="treeProps"
:current-node-key="currentNodeKey"
node-key="deptId"
highlight-current
@node-click="handleCollegeNodeClick"
class="college-tree"
>
<template #default="{ node, data }">
<div class="flex items-start py-1">
<span class="text-sm leading-6">{{ node.label }}</span>
<span class="text-xs text-gray-400 ml-2 leading-6">({{ data.deptCategory }})</span>
</div>
</template>
</el-tree>
</el-card>
</div>
<!-- 右侧专业列表 -->
<div class="flex-1">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="专业名称" prop="deptName">
<el-input v-model="queryParams.deptName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="部门类别" prop="deptCategory">
<el-input v-model="queryParams.deptCategory" placeholder="请输入部门类别" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="正常" :value="'0'" />
<el-option label="停用" :value="'1'" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['majormsg:major:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['majormsg:major:edit']"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['majormsg:major:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Connection"
:disabled="ids.length < 2"
@click="handleMergeOpen"
v-hasPermi="['majormsg:major:merge']"
>专业合并</el-button
>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<div class="table-wrapper">
<el-table v-loading="loading" border :data="majorList" @selection-change="handleSelectionChange" style="min-width: 900px; width: auto">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="部门ID" align="center" prop="deptId" :min-width="120" />
<el-table-column label="专业名称" align="center" prop="deptName" :min-width="220" />
<el-table-column label="部门类别" align="center" prop="deptCategory" :min-width="150" />
<el-table-column label="所属学院" align="center" prop="parentName" :min-width="180" />
<el-table-column label="显示顺序" align="center" prop="orderNum" :min-width="100" />
<el-table-column label="状态" align="center" prop="status" :min-width="100">
<template #default="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
{{ scope.row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="255" fixed="right">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button
link
type="primary"
plain
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['majormsg:major:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
type="danger"
plain
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['majormsg:major:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
</div>
<!-- 专业合并对话框 -->
<el-dialog title="专业合并" v-model="mergeDialog.visible" width="720px" append-to-body destroy-on-close>
<el-alert
type="warning"
:closable="false"
show-icon
title="合并说明"
description="将把源专业下的班级挂到目标专业(班级ID不变、同名班不合并);学生及业务表中的专业ID与专业名称同步改为目标专业;源专业将被停用。操作前会写入行级快照。"
class="mb-3"
/>
<el-form label-width="100px">
<el-form-item label="目标专业" required>
<el-radio-group v-model="mergeDialog.targetMajorId" @change="loadMergePreview">
<el-radio v-for="item in mergeDialog.selectedRows" :key="item.deptId" :value="item.deptId" class="mr-4 mb-2">
{{ item.deptName }}({{ item.deptCategory }})
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="已选专业">
<el-tag v-for="item in mergeDialog.selectedRows" :key="'tag-' + item.deptId" class="mr-2 mb-1" type="info">
{{ item.deptName }}
</el-tag>
</el-form-item>
</el-form>
<div v-if="mergeDialog.preview" class="merge-preview">
<div class="mb-2 text-sm">
影响预览:班级 {{ mergeDialog.preview.totalClassCount }} 个,学生 {{ mergeDialog.preview.totalStudentCount }} 人
</div>
<el-table :data="mergeDialog.preview.sources" border size="small" class="mb-3">
<el-table-column label="源专业" prop="majorName" min-width="160" />
<el-table-column label="类别编码" prop="majorCategory" min-width="140" />
<el-table-column label="班级数" prop="classCount" width="90" align="center" />
<el-table-column label="学生数" prop="studentCount" width="90" align="center" />
</el-table>
<el-table v-if="mergeDialog.preview.tableHits?.length" :data="mergeDialog.preview.tableHits" border size="small" max-height="220">
<el-table-column label="业务表" prop="table" min-width="220" />
<el-table-column label="策略" prop="strategy" width="120" />
<el-table-column label="影响行数" prop="rows" width="100" align="center" />
</el-table>
<div v-if="mergeDialog.preview.warnings?.length" class="mt-2 text-xs text-gray-500">
<div v-for="(w, idx) in mergeDialog.preview.warnings" :key="idx">• {{ w }}</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button :loading="mergeDialog.previewLoading" @click="loadMergePreview">刷新预览</el-button>
<el-button :loading="mergeDialog.submitting" type="primary" @click="submitMerge">确认合并</el-button>
<el-button @click="mergeDialog.visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 添加或修改专业对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="majorFormRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="所属学院" prop="parentId">
<el-select v-model="form.parentId" placeholder="请选择所属学院" style="width: 100%" @change="handleCollegeChange">
<el-option v-for="item in collegeOptions" :key="item.deptId" :label="item.deptName" :value="item.deptId" />
</el-select>
</el-form-item>
<el-form-item label="专业名称" prop="deptName">
<el-input v-model="form.deptName" placeholder="请输入专业名称" />
</el-form-item>
<el-form-item label="部门类别" prop="deptCategory">
<el-input v-model="form.deptCategory" placeholder="请输入部门类别编码(zy-开头)" />
</el-form-item>
<el-form-item label="显示顺序" prop="orderNum">
<el-input-number v-model="form.orderNum" :min="0" :max="999" controls-position="right" placeholder="请输入显示顺序" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
<el-option label="正常" :value="'0'" />
<el-option label="停用" :value="'1'" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="MajorMsg" lang="ts">
import { listMajor, getMajor, delMajor, addMajor, updateMajor, getCollegeTree, previewMajorMerge, mergeMajors } from '@/api/xg/xsjcxx/classMsg/majormsg';
import { MajorVO, MajorQuery, MajorForm, CollegeTreeNode, MajorMergePreviewVO } from '@/api/xg/xsjcxx/classMsg/majormsg/types';
import { ComponentInternalInstance, ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue';
import type { FormInstance as ElFormInstance } from 'element-plus';
import type { ElTree } from 'element-plus';
interface DialogOption {
visible: boolean;
title: string;
}
interface PageData<T, Q> {
form: T;
queryParams: Q;
rules: Record<string, any[]>;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const majorList = ref<MajorVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(false);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const selectedRows = ref<MajorVO[]>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const collegeTreeData = ref<CollegeTreeNode[]>([]);
const collegeOptions = ref<MajorVO[]>([]);
const selectedCollegeId = ref<number | string>('');
const currentNodeKey = ref<number | string | null>(null);
const queryFormRef = ref<ElFormInstance>();
const majorFormRef = ref<ElFormInstance>();
const collegeTreeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'deptName'
};
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const mergeDialog = reactive({
visible: false,
targetMajorId: undefined as string | number | undefined,
selectedRows: [] as MajorVO[],
preview: null as MajorMergePreviewVO | null,
previewLoading: false,
submitting: false
});
const initFormData: MajorForm = {
deptId: undefined,
parentId: undefined,
deptName: undefined,
deptCategory: 'zy-',
orderNum: 0,
status: '0',
delFlag: undefined,
ancestors: undefined
};
const data = reactive<PageData<MajorForm, MajorQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
deptName: undefined,
deptCategory: undefined,
deptCategoryLike: 'zy-',
parentId: undefined,
status: undefined,
params: {}
},
rules: {
parentId: [{ required: true, message: '所属学院不能为空', trigger: 'change' }],
deptName: [{ required: true, message: '专业名称不能为空', trigger: 'blur' }],
deptCategory: [{ required: true, message: '部门类别不能为空', trigger: 'blur' }],
orderNum: [{ required: true, message: '显示顺序不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询专业列表 */
const getList = async () => {
// 确保学院数据已加载
if (collegeOptions.value.length === 0) {
await getCollegeList();
}
loading.value = true;
const res: any = await listMajor(queryParams.value);
// 处理后端返回的数据:由于后端接口不支持分页,返回所有数据,需要前端实现分页
let allMajors = (res && (res.rows || res.data)) || [];
const totalCount = allMajors.length;
// 为每个专业设置所属学院名称
allMajors = allMajors.map((major: MajorVO) => {
const college = collegeOptions.value.find((college) => college.deptId === major.parentId);
return {
...major,
parentName: college ? college.deptName : ''
};
});
// 前端分页:计算当前页的数据
const { pageNum, pageSize } = queryParams.value;
const startIndex = (pageNum - 1) * pageSize;
const endIndex = startIndex + pageSize;
const pageMajors = allMajors.slice(startIndex, endIndex);
majorList.value = pageMajors;
total.value = totalCount;
loading.value = false;
};
/** 获取学院树形结构 */
const getCollegeList = async () => {
const res = await getCollegeTree();
collegeTreeData.value = res.data || [];
collegeOptions.value = res.data || [];
};
/** 学院树节点点击事件 */
const handleCollegeNodeClick = (data: CollegeTreeNode) => {
selectedCollegeId.value = data.deptId as string;
currentNodeKey.value = data.deptId as string;
queryParams.value.parentId = data.deptId as number;
getList();
};
/** 学院选择框变化事件 */
const handleCollegeChange = (value: number) => {
if (value) {
const college = collegeOptions.value.find((item) => item.deptId === value);
if (college) {
form.value.parentName = college.deptName;
}
} else {
form.value.parentName = undefined;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
majorFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
// 只重置搜索条件,保持选中的学院不变
queryParams.value.deptName = undefined;
queryParams.value.deptCategory = undefined;
queryParams.value.pageNum = 1;
// 重置后重新查询当前选中的学院
getList();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: MajorVO[]) => {
ids.value = selection.map((item) => item.deptId);
selectedRows.value = selection;
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 打开专业合并弹窗 */
const handleMergeOpen = async () => {
if (selectedRows.value.length < 2) {
proxy?.$modal.msgWarning('请至少选择两个专业');
return;
}
const collegeIds = [...new Set(selectedRows.value.map((item) => String(item.parentId)))];
if (collegeIds.length > 1) {
proxy?.$modal.msgWarning('仅允许同一学院下的专业合并');
return;
}
mergeDialog.selectedRows = [...selectedRows.value];
mergeDialog.targetMajorId = mergeDialog.selectedRows[0]?.deptId;
mergeDialog.preview = null;
mergeDialog.visible = true;
await loadMergePreview();
};
/** 加载合并影响预览 */
const loadMergePreview = async () => {
if (!mergeDialog.targetMajorId) {
proxy?.$modal.msgWarning('请选择目标专业');
return;
}
mergeDialog.previewLoading = true;
try {
const res: any = await previewMajorMerge({
targetMajorId: mergeDialog.targetMajorId,
selectedMajorIds: mergeDialog.selectedRows.map((item) => item.deptId)
});
mergeDialog.preview = res.data;
} finally {
mergeDialog.previewLoading = false;
}
};
/** 提交专业合并 */
const submitMerge = async () => {
if (!mergeDialog.targetMajorId) {
proxy?.$modal.msgWarning('请选择目标专业');
return;
}
const target = mergeDialog.selectedRows.find((item) => item.deptId === mergeDialog.targetMajorId);
const sources = mergeDialog.selectedRows.filter((item) => item.deptId !== mergeDialog.targetMajorId);
const sourceNames = sources.map((item) => item.deptName).join('、');
await proxy?.$modal.confirm(
`确认将【${sourceNames}】合并到目标专业【${target?.deptName}】?\n源专业将停用;班级只改归属不做班级合并;专业ID与名称将同步更新。此操作写入快照后执行。`
);
mergeDialog.submitting = true;
try {
const res: any = await mergeMajors({
targetMajorId: mergeDialog.targetMajorId,
selectedMajorIds: mergeDialog.selectedRows.map((item) => item.deptId)
});
const data = res.data || {};
proxy?.$modal.msgSuccess(
`合并成功:迁移班级 ${data.movedClassCount || 0} 个,更新学生 ${data.updatedStudentCount || 0} 人,快照 ${data.snapshotRowCount || 0} 行(批次 ${data.mergeBatchId || '-'})`
);
mergeDialog.visible = false;
await getList();
} finally {
mergeDialog.submitting = false;
}
};
/** 新增按钮操作 */
const handleAdd = () => {
if (!selectedCollegeId.value) {
proxy?.$modal.msgWarning('请先选择左侧的二级学院');
return;
}
reset();
form.value.parentId = selectedCollegeId.value;
dialog.visible = true;
dialog.title = '添加专业';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: MajorVO) => {
reset();
const _deptId = row?.deptId || ids.value[0];
const res = await getMajor(_deptId);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改专业';
};
/** 提交按钮 */
const submitForm = () => {
majorFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.deptId) {
await updateMajor(form.value).finally(() => (buttonLoading.value = false));
} else {
await addMajor(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: MajorVO) => {
const _deptIds = row?.deptId || ids.value;
// 获取要删除的专业名称
let deptNames = '';
if (row) {
// 单个删除时直接使用行数据中的专业名称
deptNames = row.deptName;
} else {
// 批量删除时,根据ids查找对应的专业名称并拼接
deptNames = majorList.value
.filter((item) => ids.value.includes(item.deptId))
.map((item) => item.deptName)
.join('、');
}
await proxy?.$modal.confirm('是否确认删除专业"' + deptNames + '"?').finally(() => (loading.value = false));
await delMajor(_deptIds);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
onMounted(() => {
getCollegeList();
// 页面初始化时不加载专业数据,等待用户选择学院
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
height: calc(100vh - 120px);
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
/* 学院树样式 */
.college-tree {
height: calc(100vh - 200px);
overflow-y: auto;
}
:deep(.el-tree-node__content) {
height: 40px;
padding: 0 8px;
}
:deep(.el-tree-node__label) {
font-size: 14px;
}
/* 表格布局修复:固定表格布局并允许横向滚动,避免无数据时列宽异常扩展 */
.table-wrapper {
overflow-x: auto;
}
/* 防止空数据时 el-table 的空白块被拉伸过宽 */
.table-wrapper :deep(.el-table__empty-block) {
min-width: 0 !important;
width: auto !important;
max-width: 100% !important;
display: flex;
justify-content: center;
}
.table-wrapper :deep(.el-table) {
width: auto !important;
}
/* 使用固定布局防止列在空数据时被浏览器不稳定拉伸 */
.table-wrapper :deep(.el-table) {
table-layout: fixed !important;
width: 100% !important;
}
.table-wrapper :deep(.el-table__body-wrapper) {
overflow-x: auto;
}
</style>
<style scoped>
/* 选中节点样式:加深背景并突出文字(覆盖 element-plus 默认) */
::deep(.el-tree-node__content.is-current) {
background-color: #409eff !important;
border-left: 4px solid #2b7cd1 !important;
border-radius: 4px 0 0 4px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* 覆盖Element Plus默认的浅色选中样式 */
::deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
background-color: #409eff !important;
}
::deep(.el-tree-node__content.is-current .el-tree-node__label) {
color: #409eff;
font-weight: 600;
}
</style>