feat: 完善学校管理模块(根据需求文档)

后端修改:
1. PgRegionController: 新增 /tree 接口用于获取区域树
2. PgRegion: 添加 children 字段支持树形结构
3. PgRegionService: 实现 selectTree 方法构建树形数据
4. PgSchoolController: 新增学校年级/班级管理接口
   - GET /{schoolId}/grades: 获取学校的年级列表
   - POST /grade: 为学校批量添加年级
   - POST /class: 为年级批量添加班级
5. PgSchoolService: 实现年级班级挂载逻辑
6. PgSchoolGrade: 添加 gradeName 字段用于显示

前端修改:
1. school.js API: 修正路径 /api → /business
2. index.vue: 修正删除接口的 ID 字段(id → schoolId)
3. SchoolDialog.vue: 修正表单字段名
4. GradeDialog.vue: 修正 API 调用和数据格式
5. ClassDialog.vue: 重写支持选择学校已挂载的年级

@author pangu
This commit is contained in:
神码-方晓辉 2026-02-02 16:32:33 +08:00
parent 82a0cb7f07
commit bd14bb36c4
13 changed files with 230 additions and 38 deletions

View File

@ -32,6 +32,14 @@ public class PgRegionController extends BaseController {
return R.ok(regionService.selectList(region)); return R.ok(regionService.selectList(region));
} }
/**
* 获取区域树用于学校管理左侧筛选
*/
@GetMapping("/tree")
public R<List<PgRegion>> tree() {
return R.ok(regionService.selectTree());
}
@GetMapping("/children/{parentId}") @GetMapping("/children/{parentId}")
public R<List<PgRegion>> children(@PathVariable Long parentId) { public R<List<PgRegion>> children(@PathVariable Long parentId) {
return R.ok(regionService.selectByParentId(parentId)); return R.ok(regionService.selectByParentId(parentId));

View File

@ -1,6 +1,7 @@
package org.dromara.pangu.base.domain; package org.dromara.pangu.base.domain;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
@ -8,6 +9,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.util.List;
/** /**
* 区域表 * 区域表
* *
@ -39,4 +42,10 @@ public class PgRegion extends BaseEntity {
@TableLogic @TableLogic
private String delFlag; private String delFlag;
/**
* 子区域列表非数据库字段
*/
@TableField(exist = false)
private List<PgRegion> children;
} }

View File

@ -11,6 +11,7 @@ import java.util.List;
*/ */
public interface IPgRegionService { public interface IPgRegionService {
List<PgRegion> selectList(PgRegion region); List<PgRegion> selectList(PgRegion region);
List<PgRegion> selectTree();
PgRegion selectById(Long regionId); PgRegion selectById(Long regionId);
List<PgRegion> selectByParentId(Long parentId); List<PgRegion> selectByParentId(Long parentId);
int insert(PgRegion region); int insert(PgRegion region);

View File

@ -1,6 +1,7 @@
package org.dromara.pangu.base.service.impl; package org.dromara.pangu.base.service.impl;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.pangu.base.domain.PgRegion; import org.dromara.pangu.base.domain.PgRegion;
@ -8,8 +9,10 @@ import org.dromara.pangu.base.mapper.PgRegionMapper;
import org.dromara.pangu.base.service.IPgRegionService; import org.dromara.pangu.base.service.IPgRegionService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* 区域 Service 实现 * 区域 Service 实现
@ -32,6 +35,28 @@ public class PgRegionServiceImpl implements IPgRegionService {
return baseMapper.selectList(lqw); return baseMapper.selectList(lqw);
} }
@Override
public List<PgRegion> selectTree() {
// 查询所有启用的区域
List<PgRegion> allRegions = baseMapper.selectList(
new LambdaQueryWrapper<PgRegion>()
.eq(PgRegion::getStatus, "0")
.orderByAsc(PgRegion::getOrderNum)
);
// 构建树形结构
return buildTree(allRegions, 0L);
}
/**
* 构建区域树
*/
private List<PgRegion> buildTree(List<PgRegion> regions, Long parentId) {
return regions.stream()
.filter(r -> parentId.equals(r.getParentId()))
.peek(r -> r.setChildren(buildTree(regions, r.getRegionId())))
.collect(Collectors.toList());
}
@Override @Override
public PgRegion selectById(Long regionId) { public PgRegion selectById(Long regionId) {
return baseMapper.selectById(regionId); return baseMapper.selectById(regionId);

View File

@ -9,11 +9,14 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController; import org.dromara.common.web.core.BaseController;
import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolClass;
import org.dromara.pangu.school.domain.PgSchoolGrade;
import org.dromara.pangu.school.service.IPgSchoolService; import org.dromara.pangu.school.service.IPgSchoolService;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 学校管理 * 学校管理
@ -65,4 +68,39 @@ public class PgSchoolController extends BaseController {
public R<Void> remove(@PathVariable Long[] schoolIds) { public R<Void> remove(@PathVariable Long[] schoolIds) {
return toAjax(schoolService.deleteByIds(schoolIds)); return toAjax(schoolService.deleteByIds(schoolIds));
} }
/**
* 获取学校的年级列表
*/
@GetMapping("/{schoolId}/grades")
public R<List<PgSchoolGrade>> getSchoolGrades(@PathVariable Long schoolId) {
return R.ok(schoolService.selectGradesBySchoolId(schoolId));
}
/**
* 为学校添加年级批量挂载
*/
@SaCheckPermission("business:school:edit")
@Log(title = "学校年级管理", businessType = BusinessType.INSERT)
@PostMapping("/grade")
public R<Void> addSchoolGrade(@RequestBody Map<String, Object> params) {
Long schoolId = Long.valueOf(params.get("schoolId").toString());
@SuppressWarnings("unchecked")
List<Long> gradeIds = (List<Long>) params.get("gradeIds");
return toAjax(schoolService.addSchoolGrades(schoolId, gradeIds));
}
/**
* 为年级添加班级批量挂载
*/
@SaCheckPermission("business:school:edit")
@Log(title = "学校班级管理", businessType = BusinessType.INSERT)
@PostMapping("/class")
public R<Void> addGradeClass(@RequestBody Map<String, Object> params) {
Long schoolId = Long.valueOf(params.get("schoolId").toString());
Long schoolGradeId = Long.valueOf(params.get("schoolGradeId").toString());
@SuppressWarnings("unchecked")
List<Long> classIds = (List<Long>) params.get("classIds");
return toAjax(schoolService.addGradeClasses(schoolId, schoolGradeId, classIds));
}
} }

View File

@ -1,6 +1,7 @@
package org.dromara.pangu.school.domain; package org.dromara.pangu.school.domain;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
@ -29,4 +30,10 @@ public class PgSchoolGrade implements Serializable {
private Long createBy; private Long createBy;
private Date createTime; private Date createTime;
/**
* 年级名称非数据库字段用于前端显示
*/
@TableField(exist = false)
private String gradeName;
} }

View File

@ -3,6 +3,7 @@ package org.dromara.pangu.school.service;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolGrade;
import java.util.List; import java.util.List;
@ -18,4 +19,19 @@ public interface IPgSchoolService {
int insert(PgSchool school); int insert(PgSchool school);
int update(PgSchool school); int update(PgSchool school);
int deleteByIds(Long[] schoolIds); int deleteByIds(Long[] schoolIds);
/**
* 获取学校的年级列表
*/
List<PgSchoolGrade> selectGradesBySchoolId(Long schoolId);
/**
* 为学校添加年级批量挂载
*/
int addSchoolGrades(Long schoolId, List<Long> gradeIds);
/**
* 为年级添加班级批量挂载
*/
int addGradeClasses(Long schoolId, Long schoolGradeId, List<Long> classIds);
} }

View File

@ -6,10 +6,17 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.pangu.base.domain.PgGrade;
import org.dromara.pangu.base.mapper.PgGradeMapper;
import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolClass;
import org.dromara.pangu.school.domain.PgSchoolGrade;
import org.dromara.pangu.school.mapper.PgSchoolClassMapper;
import org.dromara.pangu.school.mapper.PgSchoolGradeMapper;
import org.dromara.pangu.school.mapper.PgSchoolMapper; import org.dromara.pangu.school.mapper.PgSchoolMapper;
import org.dromara.pangu.school.service.IPgSchoolService; import org.dromara.pangu.school.service.IPgSchoolService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -24,6 +31,9 @@ import java.util.List;
public class PgSchoolServiceImpl implements IPgSchoolService { public class PgSchoolServiceImpl implements IPgSchoolService {
private final PgSchoolMapper baseMapper; private final PgSchoolMapper baseMapper;
private final PgSchoolGradeMapper schoolGradeMapper;
private final PgSchoolClassMapper schoolClassMapper;
private final PgGradeMapper gradeMapper;
@Override @Override
public TableDataInfo<PgSchool> selectPageList(PgSchool school, PageQuery pageQuery) { public TableDataInfo<PgSchool> selectPageList(PgSchool school, PageQuery pageQuery) {
@ -57,6 +67,66 @@ public class PgSchoolServiceImpl implements IPgSchoolService {
return baseMapper.deleteByIds(Arrays.asList(schoolIds)); return baseMapper.deleteByIds(Arrays.asList(schoolIds));
} }
@Override
public List<PgSchoolGrade> selectGradesBySchoolId(Long schoolId) {
List<PgSchoolGrade> schoolGrades = schoolGradeMapper.selectList(
new LambdaQueryWrapper<PgSchoolGrade>()
.eq(PgSchoolGrade::getSchoolId, schoolId)
);
// 关联查询年级名称
for (PgSchoolGrade sg : schoolGrades) {
PgGrade grade = gradeMapper.selectById(sg.getGradeId());
if (grade != null) {
sg.setGradeName(grade.getGradeName());
}
}
return schoolGrades;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int addSchoolGrades(Long schoolId, List<Long> gradeIds) {
int count = 0;
for (Long gradeId : gradeIds) {
// 检查是否已存在
Long exists = schoolGradeMapper.selectCount(
new LambdaQueryWrapper<PgSchoolGrade>()
.eq(PgSchoolGrade::getSchoolId, schoolId)
.eq(PgSchoolGrade::getGradeId, gradeId)
);
if (exists == 0) {
PgSchoolGrade schoolGrade = new PgSchoolGrade();
schoolGrade.setSchoolId(schoolId);
schoolGrade.setGradeId(gradeId);
count += schoolGradeMapper.insert(schoolGrade);
}
}
return count;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int addGradeClasses(Long schoolId, Long schoolGradeId, List<Long> classIds) {
int count = 0;
for (Long classId : classIds) {
// 检查是否已存在
Long exists = schoolClassMapper.selectCount(
new LambdaQueryWrapper<PgSchoolClass>()
.eq(PgSchoolClass::getSchoolId, schoolId)
.eq(PgSchoolClass::getSchoolGradeId, schoolGradeId)
.eq(PgSchoolClass::getClassId, classId)
);
if (exists == 0) {
PgSchoolClass schoolClass = new PgSchoolClass();
schoolClass.setSchoolId(schoolId);
schoolClass.setSchoolGradeId(schoolGradeId);
schoolClass.setClassId(classId);
count += schoolClassMapper.insert(schoolClass);
}
}
return count;
}
private LambdaQueryWrapper<PgSchool> buildQueryWrapper(PgSchool school) { private LambdaQueryWrapper<PgSchool> buildQueryWrapper(PgSchool school) {
LambdaQueryWrapper<PgSchool> lqw = new LambdaQueryWrapper<>(); LambdaQueryWrapper<PgSchool> lqw = new LambdaQueryWrapper<>();
lqw.like(StrUtil.isNotBlank(school.getSchoolName()), PgSchool::getSchoolName, school.getSchoolName()); lqw.like(StrUtil.isNotBlank(school.getSchoolName()), PgSchool::getSchoolName, school.getSchoolName());

View File

@ -7,7 +7,7 @@ import request from '@/utils/request'
// 获取区域树 // 获取区域树
export function getRegionTree() { export function getRegionTree() {
return request({ return request({
url: '/api/region/tree', url: '/business/region/tree',
method: 'get' method: 'get'
}) })
} }
@ -15,7 +15,7 @@ export function getRegionTree() {
// 获取学校列表 // 获取学校列表
export function getSchoolList(params) { export function getSchoolList(params) {
return request({ return request({
url: '/api/school/list', url: '/business/school/list',
method: 'get', method: 'get',
params params
}) })
@ -24,7 +24,7 @@ export function getSchoolList(params) {
// 获取学校详情 // 获取学校详情
export function getSchoolDetail(id) { export function getSchoolDetail(id) {
return request({ return request({
url: `/api/school/${id}`, url: `/business/school/${id}`,
method: 'get' method: 'get'
}) })
} }
@ -32,7 +32,7 @@ export function getSchoolDetail(id) {
// 新增学校 // 新增学校
export function addSchool(data) { export function addSchool(data) {
return request({ return request({
url: '/api/school', url: '/business/school',
method: 'post', method: 'post',
data data
}) })
@ -41,7 +41,7 @@ export function addSchool(data) {
// 修改学校 // 修改学校
export function updateSchool(data) { export function updateSchool(data) {
return request({ return request({
url: '/api/school', url: '/business/school',
method: 'put', method: 'put',
data data
}) })
@ -50,15 +50,15 @@ export function updateSchool(data) {
// 删除学校 // 删除学校
export function deleteSchool(id) { export function deleteSchool(id) {
return request({ return request({
url: `/api/school/${id}`, url: `/business/school/${id}`,
method: 'delete' method: 'delete'
}) })
} }
// 获取年级列表(字典数据 // 获取年级选项(用于学校挂载年级
export function getGradeOptions() { export function getGradeOptions() {
return request({ return request({
url: '/api/grade/options', url: '/business/grade/listAll',
method: 'get' method: 'get'
}) })
} }
@ -66,16 +66,16 @@ export function getGradeOptions() {
// 为学校添加年级 // 为学校添加年级
export function addSchoolGrade(data) { export function addSchoolGrade(data) {
return request({ return request({
url: '/api/school/grade', url: '/business/school/grade',
method: 'post', method: 'post',
data data
}) })
} }
// 获取班级列表(字典数据 // 获取班级选项(用于年级挂载班级
export function getClassOptions() { export function getClassOptions() {
return request({ return request({
url: '/api/class/options', url: '/business/class/listAll',
method: 'get' method: 'get'
}) })
} }
@ -83,8 +83,16 @@ export function getClassOptions() {
// 为年级添加班级 // 为年级添加班级
export function addGradeClass(data) { export function addGradeClass(data) {
return request({ return request({
url: '/api/school/class', url: '/business/school/class',
method: 'post', method: 'post',
data data
}) })
} }
// 获取学校年级树(用于新增班级时选择年级)
export function getSchoolGradeTree(schoolId) {
return request({
url: `/business/school/${schoolId}/grades`,
method: 'get'
})
}

View File

@ -12,13 +12,13 @@
</div> </div>
<el-form ref="formRef" :model="form" label-width="80px"> <el-form ref="formRef" :model="form" label-width="80px">
<el-form-item label="选择年级" prop="gradeId"> <el-form-item label="选择年级" prop="schoolGradeId">
<el-select v-model="form.gradeId" placeholder="请先选择年级" style="width: 100%"> <el-select v-model="form.schoolGradeId" placeholder="请先选择年级" style="width: 100%">
<el-option <el-option
v-for="item in gradeOptions" v-for="item in schoolGradeOptions"
:key="item.value" :key="item.id"
:label="item.label" :label="item.gradeName"
:value="item.value" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -50,7 +50,8 @@
*/ */
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { addGradeClass, getClassOptions, getGradeOptions } from '@/api/pangu/school' import request from '@/utils/request'
import { addGradeClass, getClassOptions, getSchoolGradeTree } from '@/api/pangu/school'
const emit = defineEmits(['success']) const emit = defineEmits(['success'])
@ -58,25 +59,25 @@ const dialogVisible = ref(false)
const submitLoading = ref(false) const submitLoading = ref(false)
const formRef = ref(null) const formRef = ref(null)
const currentSchool = ref(null) const currentSchool = ref(null)
const gradeOptions = ref([]) const schoolGradeOptions = ref([])
const classOptions = ref([]) const classOptions = ref([])
// //
const form = ref({ const form = ref({
schoolId: null, schoolId: null,
gradeId: '', schoolGradeId: null,
classIds: [] classIds: []
}) })
// //
const fetchGradeOptions = async () => { const fetchSchoolGrades = async (schoolId) => {
try { try {
const res = await getGradeOptions() const res = await getSchoolGradeTree(schoolId)
if (res.code === 200) { if (res.code === 200) {
gradeOptions.value = res.data schoolGradeOptions.value = res.data || []
} }
} catch (error) { } catch (error) {
console.error('获取年级选项失败:', error) console.error('获取学校年级失败:', error)
} }
} }
@ -85,7 +86,11 @@ const fetchClassOptions = async () => {
try { try {
const res = await getClassOptions() const res = await getClassOptions()
if (res.code === 200) { if (res.code === 200) {
classOptions.value = res.data //
classOptions.value = (res.data || []).map(item => ({
value: item.classId,
label: item.className
}))
} }
} catch (error) { } catch (error) {
console.error('获取班级选项失败:', error) console.error('获取班级选项失败:', error)
@ -97,15 +102,17 @@ const open = (school) => {
dialogVisible.value = true dialogVisible.value = true
currentSchool.value = school currentSchool.value = school
form.value = { form.value = {
schoolId: school.id, schoolId: school.schoolId,
gradeId: '', schoolGradeId: null,
classIds: [] classIds: []
} }
//
fetchSchoolGrades(school.schoolId)
} }
// //
const handleSubmit = async () => { const handleSubmit = async () => {
if (!form.value.gradeId) { if (!form.value.schoolGradeId) {
ElMessage.warning('请选择年级') ElMessage.warning('请选择年级')
return return
} }
@ -131,7 +138,6 @@ const handleSubmit = async () => {
// //
onMounted(() => { onMounted(() => {
fetchGradeOptions()
fetchClassOptions() fetchClassOptions()
}) })

View File

@ -60,7 +60,11 @@ const fetchGradeOptions = async () => {
try { try {
const res = await getGradeOptions() const res = await getGradeOptions()
if (res.code === 200) { if (res.code === 200) {
gradeOptions.value = res.data //
gradeOptions.value = (res.data || []).map(item => ({
value: item.gradeId,
label: item.gradeName
}))
} }
} catch (error) { } catch (error) {
console.error('获取年级选项失败:', error) console.error('获取年级选项失败:', error)
@ -72,7 +76,7 @@ const open = (school) => {
dialogVisible.value = true dialogVisible.value = true
currentSchool.value = school currentSchool.value = school
form.value = { form.value = {
schoolId: school.id, schoolId: school.schoolId,
gradeIds: [] gradeIds: []
} }
} }

View File

@ -83,7 +83,7 @@ const isEdit = ref(false)
// //
const form = ref({ const form = ref({
id: null, schoolId: null,
schoolName: '', schoolName: '',
schoolType: '', schoolType: '',
regionId: null, regionId: null,
@ -144,7 +144,7 @@ const open = (row) => {
if (row) { if (row) {
// //
form.value = { form.value = {
id: row.id, schoolId: row.schoolId,
schoolName: row.schoolName, schoolName: row.schoolName,
schoolType: row.schoolType, schoolType: row.schoolType,
regionId: row.regionId, regionId: row.regionId,
@ -155,7 +155,7 @@ const open = (row) => {
} else { } else {
// //
form.value = { form.value = {
id: null, schoolId: null,
schoolName: '', schoolName: '',
schoolType: '', schoolType: '',
regionId: null, regionId: null,
@ -203,7 +203,7 @@ const handleSubmit = async () => {
regionIds: undefined // regionIds: undefined //
} }
const res = isEdit.value const res = form.value.schoolId
? await updateSchool(submitData) ? await updateSchool(submitData)
: await addSchool(submitData) : await addSchool(submitData)

View File

@ -216,7 +216,7 @@ const handleDelete = (row) => {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(async () => { }).then(async () => {
const res = await request.delete(`/business/school/${row.id}`) const res = await request.delete(`/business/school/${row.schoolId}`)
if (res.code === 200) { if (res.code === 200) {
ElMessage.success('删除成功') ElMessage.success('删除成功')
getList() getList()