feat: 会员管理-添加学生绑定功能
1. 后端新增接口:
- GET /business/student/available - 查询可绑定的学生列表
- GET /business/student/byMember/{memberId} - 查询会员已绑定的学生
- GET /business/member/{memberId}/students - 获取会员已绑定的学生列表
- POST /business/member/{memberId}/bindStudents - 批量绑定学生到会员
- POST /business/member/unbindStudent/{studentId} - 解绑学生
- GET /business/school/grade/{schoolGradeId}/classes - 获取年级下的班级列表
2. 业务规则实现:
- 教师身份只能绑定同校学生(schoolId相同)
- 家长身份可绑定任意学生
- 一个学生只能归属一个会员
3. 前端功能:
- 新增StudentSelectDialog学生选择器组件
- 支持按姓名、学号搜索
- 支持多选绑定
- MemberDialog集成学生选择器
4. 修复问题:
- 修复会员编辑时学校信息(区域/年级/班级)回显问题
- 修复班级列表接口404问题
This commit is contained in:
parent
d28b68ef46
commit
2baf792159
|
|
@ -10,10 +10,13 @@ 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.member.domain.PgMember;
|
import org.dromara.pangu.member.domain.PgMember;
|
||||||
import org.dromara.pangu.member.service.IPgMemberService;
|
import org.dromara.pangu.member.service.IPgMemberService;
|
||||||
|
import org.dromara.pangu.student.domain.PgStudent;
|
||||||
|
import org.dromara.pangu.student.service.IPgStudentService;
|
||||||
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.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,6 +31,7 @@ import java.util.Map;
|
||||||
public class PgMemberController extends BaseController {
|
public class PgMemberController extends BaseController {
|
||||||
|
|
||||||
private final IPgMemberService memberService;
|
private final IPgMemberService memberService;
|
||||||
|
private final IPgStudentService studentService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询会员列表
|
* 查询会员列表
|
||||||
|
|
@ -107,4 +111,32 @@ public class PgMemberController extends BaseController {
|
||||||
public R<Boolean> checkPhoneUnique(@RequestParam String phone, @RequestParam(required = false) Long memberId) {
|
public R<Boolean> checkPhoneUnique(@RequestParam String phone, @RequestParam(required = false) Long memberId) {
|
||||||
return R.ok(memberService.checkPhoneUnique(phone, memberId));
|
return R.ok(memberService.checkPhoneUnique(phone, memberId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会员已绑定的学生列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/{memberId}/students")
|
||||||
|
public R<List<PgStudent>> getMemberStudents(@PathVariable Long memberId) {
|
||||||
|
return R.ok(studentService.selectByMemberId(memberId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量绑定学生到会员
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("business:member:edit")
|
||||||
|
@Log(title = "会员管理-绑定学生", businessType = BusinessType.UPDATE)
|
||||||
|
@PostMapping("/{memberId}/bindStudents")
|
||||||
|
public R<Void> bindStudents(@PathVariable Long memberId, @RequestBody List<Long> studentIds) {
|
||||||
|
return toAjax(studentService.bindStudentsToMember(memberId, studentIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑学生
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("business:member:edit")
|
||||||
|
@Log(title = "会员管理-解绑学生", businessType = BusinessType.UPDATE)
|
||||||
|
@PostMapping("/unbindStudent/{studentId}")
|
||||||
|
public R<Void> unbindStudent(@PathVariable Long studentId) {
|
||||||
|
return toAjax(studentService.unbindStudent(studentId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.dromara.pangu.member.domain;
|
package org.dromara.pangu.member.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;
|
||||||
|
|
@ -9,6 +10,7 @@ import lombok.EqualsAndHashCode;
|
||||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 会员表
|
* 会员表
|
||||||
|
|
@ -51,6 +53,12 @@ public class PgMember extends BaseEntity {
|
||||||
|
|
||||||
private Long regionId;
|
private Long regionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区域ID路径(非数据库字段,用于级联选择器回显)
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private List<Long> regionIds;
|
||||||
|
|
||||||
private Long schoolId;
|
private Long schoolId;
|
||||||
|
|
||||||
private Long schoolGradeId;
|
private Long schoolGradeId;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor;
|
||||||
import org.dromara.common.core.exception.ServiceException;
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
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.PgRegion;
|
||||||
|
import org.dromara.pangu.base.mapper.PgRegionMapper;
|
||||||
import org.dromara.pangu.member.domain.PgMember;
|
import org.dromara.pangu.member.domain.PgMember;
|
||||||
import org.dromara.pangu.member.mapper.PgMemberMapper;
|
import org.dromara.pangu.member.mapper.PgMemberMapper;
|
||||||
import org.dromara.pangu.member.service.IPgMemberService;
|
import org.dromara.pangu.member.service.IPgMemberService;
|
||||||
|
|
@ -16,7 +18,9 @@ import cn.hutool.crypto.digest.BCrypt;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -31,6 +35,7 @@ public class PgMemberServiceImpl implements IPgMemberService {
|
||||||
|
|
||||||
private final PgMemberMapper baseMapper;
|
private final PgMemberMapper baseMapper;
|
||||||
private final PgStudentMapper studentMapper;
|
private final PgStudentMapper studentMapper;
|
||||||
|
private final PgRegionMapper regionMapper;
|
||||||
|
|
||||||
private static final String DEFAULT_PASSWORD = "123456";
|
private static final String DEFAULT_PASSWORD = "123456";
|
||||||
|
|
||||||
|
|
@ -48,7 +53,29 @@ public class PgMemberServiceImpl implements IPgMemberService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PgMember selectById(Long memberId) {
|
public PgMember selectById(Long memberId) {
|
||||||
return baseMapper.selectById(memberId);
|
PgMember member = baseMapper.selectById(memberId);
|
||||||
|
if (member != null && member.getRegionId() != null) {
|
||||||
|
// 查询区域的完整路径用于级联选择器回显
|
||||||
|
member.setRegionIds(getRegionPath(member.getRegionId()));
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取区域的完整路径(从根到当前节点的ID列表)
|
||||||
|
*/
|
||||||
|
private List<Long> getRegionPath(Long regionId) {
|
||||||
|
List<Long> path = new ArrayList<>();
|
||||||
|
Long currentId = regionId;
|
||||||
|
while (currentId != null && currentId > 0) {
|
||||||
|
path.add(0, currentId);
|
||||||
|
PgRegion region = regionMapper.selectById(currentId);
|
||||||
|
if (region == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentId = region.getParentId();
|
||||||
|
}
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,14 @@ public class PgSchoolController extends BaseController {
|
||||||
return R.ok(schoolService.selectGradesBySchoolId(schoolId));
|
return R.ok(schoolService.selectGradesBySchoolId(schoolId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取年级下的班级列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/grade/{schoolGradeId}/classes")
|
||||||
|
public R<List<PgSchoolClass>> getGradeClasses(@PathVariable Long schoolGradeId) {
|
||||||
|
return R.ok(schoolService.selectClassesBySchoolGradeId(schoolGradeId));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为学校添加年级(批量挂载)
|
* 为学校添加年级(批量挂载)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -33,4 +34,10 @@ public class PgSchoolClass implements Serializable {
|
||||||
private Long createBy;
|
private Long createBy;
|
||||||
|
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级名称(非数据库字段,用于前端显示)
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String className;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,4 +50,9 @@ public interface IPgSchoolService {
|
||||||
* 删除年级下的班级
|
* 删除年级下的班级
|
||||||
*/
|
*/
|
||||||
int removeGradeClass(Long schoolClassId);
|
int removeGradeClass(Long schoolClassId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取年级下的班级列表
|
||||||
|
*/
|
||||||
|
List<org.dromara.pangu.school.domain.PgSchoolClass> selectClassesBySchoolGradeId(Long schoolGradeId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,22 @@ public class PgSchoolServiceImpl implements IPgSchoolService {
|
||||||
return schoolClassMapper.deleteById(schoolClassId);
|
return schoolClassMapper.deleteById(schoolClassId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PgSchoolClass> selectClassesBySchoolGradeId(Long schoolGradeId) {
|
||||||
|
List<PgSchoolClass> schoolClasses = schoolClassMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<PgSchoolClass>()
|
||||||
|
.eq(PgSchoolClass::getSchoolGradeId, schoolGradeId)
|
||||||
|
);
|
||||||
|
// 关联查询班级名称
|
||||||
|
for (PgSchoolClass sc : schoolClasses) {
|
||||||
|
PgClass cls = classMapper.selectById(sc.getClassId());
|
||||||
|
if (cls != null) {
|
||||||
|
sc.setClassName(cls.getClassName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schoolClasses;
|
||||||
|
}
|
||||||
|
|
||||||
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());
|
||||||
|
|
|
||||||
|
|
@ -58,4 +58,29 @@ public class PgStudentController extends BaseController {
|
||||||
public R<Void> remove(@PathVariable Long[] studentIds) {
|
public R<Void> remove(@PathVariable Long[] studentIds) {
|
||||||
return toAjax(studentService.deleteByIds(studentIds));
|
return toAjax(studentService.deleteByIds(studentIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询可绑定的学生列表(用于会员绑定学生)
|
||||||
|
* @param studentName 学生姓名(模糊查询)
|
||||||
|
* @param studentNo 学号(模糊查询)
|
||||||
|
* @param memberId 当前会员ID
|
||||||
|
* @param schoolId 学校ID(教师身份时传入,限制只能选本校学生)
|
||||||
|
*/
|
||||||
|
@GetMapping("/available")
|
||||||
|
public TableDataInfo<PgStudent> availableStudents(
|
||||||
|
@RequestParam(required = false) String studentName,
|
||||||
|
@RequestParam(required = false) String studentNo,
|
||||||
|
@RequestParam(required = false) Long memberId,
|
||||||
|
@RequestParam(required = false) Long schoolId,
|
||||||
|
PageQuery pageQuery) {
|
||||||
|
return studentService.selectAvailableStudents(studentName, studentNo, memberId, schoolId, pageQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询会员已绑定的学生列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/byMember/{memberId}")
|
||||||
|
public R<java.util.List<PgStudent>> listByMemberId(@PathVariable Long memberId) {
|
||||||
|
return R.ok(studentService.selectByMemberId(memberId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,29 @@ public interface IPgStudentService {
|
||||||
int insert(PgStudent student);
|
int insert(PgStudent student);
|
||||||
int update(PgStudent student);
|
int update(PgStudent student);
|
||||||
int deleteByIds(Long[] studentIds);
|
int deleteByIds(Long[] studentIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询可绑定的学生列表(分页)
|
||||||
|
* @param studentName 学生姓名(模糊查询)
|
||||||
|
* @param studentNo 学号(模糊查询)
|
||||||
|
* @param memberId 当前会员ID(排除已绑定其他会员的学生)
|
||||||
|
* @param schoolId 学校ID(教师身份时必传,限制只能选本校学生)
|
||||||
|
* @param pageQuery 分页参数
|
||||||
|
*/
|
||||||
|
TableDataInfo<PgStudent> selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询会员已绑定的学生列表
|
||||||
|
*/
|
||||||
|
List<PgStudent> selectByMemberId(Long memberId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量绑定学生到会员
|
||||||
|
*/
|
||||||
|
int bindStudentsToMember(Long memberId, List<Long> studentIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑学生
|
||||||
|
*/
|
||||||
|
int unbindStudent(Long studentId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,65 @@ public class PgStudentServiceImpl implements IPgStudentService {
|
||||||
lqw.orderByDesc(PgStudent::getCreateTime);
|
lqw.orderByDesc(PgStudent::getCreateTime);
|
||||||
return lqw;
|
return lqw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TableDataInfo<PgStudent> selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery) {
|
||||||
|
LambdaQueryWrapper<PgStudent> lqw = new LambdaQueryWrapper<>();
|
||||||
|
// 姓名模糊查询
|
||||||
|
lqw.like(StrUtil.isNotBlank(studentName), PgStudent::getStudentName, studentName);
|
||||||
|
// 学号模糊查询
|
||||||
|
lqw.like(StrUtil.isNotBlank(studentNo), PgStudent::getStudentNo, studentNo);
|
||||||
|
// 可绑定条件:未被绑定 或 已绑定当前会员
|
||||||
|
lqw.and(wrapper -> wrapper
|
||||||
|
.isNull(PgStudent::getMemberId)
|
||||||
|
.or()
|
||||||
|
.eq(memberId != null, PgStudent::getMemberId, memberId)
|
||||||
|
);
|
||||||
|
// 教师身份限制:只能选本校学生
|
||||||
|
lqw.eq(schoolId != null, PgStudent::getSchoolId, schoolId);
|
||||||
|
lqw.orderByDesc(PgStudent::getCreateTime);
|
||||||
|
|
||||||
|
Page<PgStudent> page = baseMapper.selectPage(pageQuery.build(), lqw);
|
||||||
|
return TableDataInfo.build(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PgStudent> selectByMemberId(Long memberId) {
|
||||||
|
if (memberId == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return baseMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<PgStudent>()
|
||||||
|
.eq(PgStudent::getMemberId, memberId)
|
||||||
|
.orderByDesc(PgStudent::getCreateTime)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int bindStudentsToMember(Long memberId, List<Long> studentIds) {
|
||||||
|
if (memberId == null || studentIds == null || studentIds.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
for (Long studentId : studentIds) {
|
||||||
|
PgStudent student = new PgStudent();
|
||||||
|
student.setStudentId(studentId);
|
||||||
|
student.setMemberId(memberId);
|
||||||
|
count += baseMapper.updateById(student);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int unbindStudent(Long studentId) {
|
||||||
|
if (studentId == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 使用原生SQL更新memberId为null
|
||||||
|
return baseMapper.update(null,
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<PgStudent>()
|
||||||
|
.eq(PgStudent::getStudentId, studentId)
|
||||||
|
.set(PgStudent::getMemberId, null)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,8 @@
|
||||||
<el-table-column prop="gradeName" label="年级" width="80" />
|
<el-table-column prop="gradeName" label="年级" width="80" />
|
||||||
<el-table-column prop="className" label="班级" width="60" />
|
<el-table-column prop="className" label="班级" width="60" />
|
||||||
<el-table-column label="操作" width="80" align="center">
|
<el-table-column label="操作" width="80" align="center">
|
||||||
<template #default="{ $index }">
|
<template #default="{ row }">
|
||||||
<el-popconfirm title="确定解绑该学生?" @confirm="handleRemoveStudent($index)">
|
<el-popconfirm title="确定解绑该学生?" @confirm="handleRemoveStudent(row)">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button link type="danger" size="small">解绑</el-button>
|
<el-button link type="danger" size="small">解绑</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -154,6 +154,9 @@
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 学生选择器 -->
|
||||||
|
<StudentSelectDialog ref="studentSelectRef" @success="loadBoundStudents" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -162,6 +165,7 @@ import { Plus } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { reactive, ref } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import StudentSelectDialog from './StudentSelectDialog.vue'
|
||||||
|
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
|
|
@ -169,6 +173,7 @@ const visible = ref(false)
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
const studentSelectRef = ref(null)
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
|
|
@ -229,10 +234,9 @@ const open = async (row) => {
|
||||||
const res = await request.get(`/business/member/${row.memberId}`)
|
const res = await request.get(`/business/member/${row.memberId}`)
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
Object.assign(form, res.data)
|
Object.assign(form, res.data)
|
||||||
// 处理区域级联
|
// 后端已返回 regionIds 数组用于级联选择器回显
|
||||||
|
// 加载学校信息的下拉选项
|
||||||
if (form.regionId) {
|
if (form.regionId) {
|
||||||
// 构建regionIds数组用于级联选择器回显
|
|
||||||
form.regionIds = [form.regionId]
|
|
||||||
await loadSchoolList(form.regionId)
|
await loadSchoolList(form.regionId)
|
||||||
}
|
}
|
||||||
if (form.schoolId) {
|
if (form.schoolId) {
|
||||||
|
|
@ -241,6 +245,8 @@ const open = async (row) => {
|
||||||
if (form.schoolGradeId) {
|
if (form.schoolGradeId) {
|
||||||
await loadClassList(form.schoolGradeId)
|
await loadClassList(form.schoolGradeId)
|
||||||
}
|
}
|
||||||
|
// 加载已绑定的学生列表
|
||||||
|
await loadBoundStudents()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载会员数据失败', e)
|
console.error('加载会员数据失败', e)
|
||||||
|
|
@ -248,6 +254,22 @@ const open = async (row) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载已绑定的学生列表
|
||||||
|
*/
|
||||||
|
const loadBoundStudents = async () => {
|
||||||
|
if (!form.memberId) {
|
||||||
|
form.students = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await request.get(`/business/member/${form.memberId}/students`)
|
||||||
|
form.students = res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
form.students = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置表单
|
* 重置表单
|
||||||
*/
|
*/
|
||||||
|
|
@ -287,8 +309,8 @@ const loadRegionTree = async () => {
|
||||||
*/
|
*/
|
||||||
const loadSchoolList = async (regionId) => {
|
const loadSchoolList = async (regionId) => {
|
||||||
try {
|
try {
|
||||||
const res = await request.get('/business/school/list', { params: { regionId } })
|
const res = await request.get('/business/school/listAll', { params: { regionId } })
|
||||||
schoolList.value = res.rows || []
|
schoolList.value = res.data || []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
schoolList.value = []
|
schoolList.value = []
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +321,7 @@ const loadSchoolList = async (regionId) => {
|
||||||
*/
|
*/
|
||||||
const loadGradeList = async (schoolId) => {
|
const loadGradeList = async (schoolId) => {
|
||||||
try {
|
try {
|
||||||
const res = await request.get('/business/school/grades', { params: { schoolId } })
|
const res = await request.get(`/business/school/${schoolId}/grades`)
|
||||||
gradeList.value = res.data || []
|
gradeList.value = res.data || []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
gradeList.value = []
|
gradeList.value = []
|
||||||
|
|
@ -311,7 +333,7 @@ const loadGradeList = async (schoolId) => {
|
||||||
*/
|
*/
|
||||||
const loadClassList = async (schoolGradeId) => {
|
const loadClassList = async (schoolGradeId) => {
|
||||||
try {
|
try {
|
||||||
const res = await request.get('/business/school/classes', { params: { schoolGradeId } })
|
const res = await request.get(`/business/school/grade/${schoolGradeId}/classes`)
|
||||||
classList.value = res.data || []
|
classList.value = res.data || []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
classList.value = []
|
classList.value = []
|
||||||
|
|
@ -379,14 +401,30 @@ const handleGradeChange = () => {
|
||||||
* 添加学生
|
* 添加学生
|
||||||
*/
|
*/
|
||||||
const handleAddStudent = () => {
|
const handleAddStudent = () => {
|
||||||
ElMessage.info('学生选择功能待开发')
|
if (!isEdit.value || !form.memberId) {
|
||||||
|
ElMessage.warning('请先保存会员信息后再绑定学生')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
studentSelectRef.value?.open({
|
||||||
|
memberId: form.memberId,
|
||||||
|
identityType: form.identityType,
|
||||||
|
schoolId: form.schoolId
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除学生
|
* 解绑学生
|
||||||
*/
|
*/
|
||||||
const handleRemoveStudent = (index) => {
|
const handleRemoveStudent = async (row) => {
|
||||||
form.students.splice(index, 1)
|
try {
|
||||||
|
const res = await request.post(`/business/member/unbindStudent/${row.studentId}`)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('解绑成功')
|
||||||
|
await loadBoundStudents()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解绑失败', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
<!--
|
||||||
|
学生选择器弹窗
|
||||||
|
@author 湖北新华业务中台研发团队
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="选择学生"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<!-- 搜索条件 -->
|
||||||
|
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||||
|
<el-form-item label="姓名">
|
||||||
|
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="学号">
|
||||||
|
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<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-form>
|
||||||
|
|
||||||
|
<!-- 教师身份提示 -->
|
||||||
|
<el-alert
|
||||||
|
v-if="isTeacher"
|
||||||
|
title="教师只能绑定本校学生"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 学生列表 -->
|
||||||
|
<el-table
|
||||||
|
ref="tableRef"
|
||||||
|
v-loading="loading"
|
||||||
|
:data="studentList"
|
||||||
|
border
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
|
<el-table-column prop="studentName" label="姓名" min-width="80" />
|
||||||
|
<el-table-column prop="studentNo" label="学号" width="120" />
|
||||||
|
<el-table-column prop="schoolName" label="学校" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="gradeName" label="年级" width="80" />
|
||||||
|
<el-table-column prop="className" label="班级" width="60" />
|
||||||
|
<el-table-column label="绑定状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.memberId === memberId" type="success" size="small">已绑定</el-tag>
|
||||||
|
<el-tag v-else-if="row.memberId" type="warning" size="small">已被绑定</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">未绑定</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="total > 0"
|
||||||
|
:current-page="queryParams.pageNum"
|
||||||
|
:page-size="queryParams.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
style="margin-top: 12px; justify-content: flex-end"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<span style="margin-right: 16px; color: #909399">已选择 {{ selectedStudents.length }} 名学生</span>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitLoading" @click="handleConfirm">确定绑定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const studentList = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const selectedStudents = ref([])
|
||||||
|
const tableRef = ref(null)
|
||||||
|
|
||||||
|
// 会员信息
|
||||||
|
const memberId = ref(null)
|
||||||
|
const isTeacher = ref(false)
|
||||||
|
const schoolId = ref(null)
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
studentName: '',
|
||||||
|
studentNo: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开弹窗
|
||||||
|
* @param options { memberId: Long, identityType: String, schoolId: Long }
|
||||||
|
*/
|
||||||
|
const open = (options = {}) => {
|
||||||
|
memberId.value = options.memberId
|
||||||
|
isTeacher.value = options.identityType === '2'
|
||||||
|
schoolId.value = options.schoolId
|
||||||
|
|
||||||
|
resetQuery()
|
||||||
|
visible.value = true
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取学生列表
|
||||||
|
*/
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
pageNum: queryParams.pageNum,
|
||||||
|
pageSize: queryParams.pageSize,
|
||||||
|
studentName: queryParams.studentName || undefined,
|
||||||
|
studentNo: queryParams.studentNo || undefined,
|
||||||
|
memberId: memberId.value,
|
||||||
|
schoolId: isTeacher.value ? schoolId.value : undefined
|
||||||
|
}
|
||||||
|
const res = await request.get('/business/student/available', { params })
|
||||||
|
studentList.value = res.rows || []
|
||||||
|
total.value = res.total || 0
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取学生列表失败', e)
|
||||||
|
studentList.value = []
|
||||||
|
total.value = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索
|
||||||
|
*/
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置搜索
|
||||||
|
*/
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
queryParams.pageSize = 10
|
||||||
|
queryParams.studentName = ''
|
||||||
|
queryParams.studentNo = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页大小变化
|
||||||
|
*/
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
queryParams.pageSize = val
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页码变化
|
||||||
|
*/
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
queryParams.pageNum = val
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择变化
|
||||||
|
*/
|
||||||
|
const handleSelectionChange = (selection) => {
|
||||||
|
selectedStudents.value = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定绑定
|
||||||
|
*/
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (selectedStudents.value.length === 0) {
|
||||||
|
ElMessage.warning('请选择要绑定的学生')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤出需要绑定的学生(排除已绑定当前会员的)
|
||||||
|
const studentIds = selectedStudents.value
|
||||||
|
.filter(s => s.memberId !== memberId.value)
|
||||||
|
.map(s => s.studentId)
|
||||||
|
|
||||||
|
if (studentIds.length === 0) {
|
||||||
|
ElMessage.info('所选学生已全部绑定')
|
||||||
|
visible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.post(`/business/member/${memberId.value}/bindStudents`, studentIds)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(`成功绑定 ${studentIds.length} 名学生`)
|
||||||
|
visible.value = false
|
||||||
|
emit('success')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue