Compare commits
22 Commits
d28b68ef46
...
e8c4f3f568
| Author | SHA1 | Date |
|---|---|---|
|
|
e8c4f3f568 | |
|
|
43ee2dbf87 | |
|
|
b5c2692aab | |
|
|
e1ea099a38 | |
|
|
904c7c9500 | |
|
|
18b9a09348 | |
|
|
a384b27033 | |
|
|
3d70bd0356 | |
|
|
6eb0643ae7 | |
|
|
4e54dc8422 | |
|
|
26efdc42fc | |
|
|
cca0902d28 | |
|
|
c18970ee98 | |
|
|
da75094367 | |
|
|
ea1524ea67 | |
|
|
eb7ef037f0 | |
|
|
4eea1eef34 | |
|
|
ca433d6ab9 | |
|
|
6784e32e1e | |
|
|
9883fddb67 | |
|
|
a9f57646fa | |
|
|
2baf792159 |
|
|
@ -10,10 +10,13 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.pangu.member.domain.PgMember;
|
||||
import org.dromara.pangu.member.service.IPgMemberService;
|
||||
import org.dromara.pangu.student.domain.vo.StudentVo;
|
||||
import org.dromara.pangu.student.service.IPgStudentService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
|
@ -28,6 +31,7 @@ import java.util.Map;
|
|||
public class PgMemberController extends BaseController {
|
||||
|
||||
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) {
|
||||
return R.ok(memberService.checkPhoneUnique(phone, memberId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员已绑定的学生列表(包含学校、年级、班级名称)
|
||||
*/
|
||||
@GetMapping("/{memberId}/students")
|
||||
public R<List<StudentVo>> 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;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
|
@ -9,6 +10,7 @@ import lombok.EqualsAndHashCode;
|
|||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会员表
|
||||
|
|
@ -51,6 +53,12 @@ public class PgMember extends BaseEntity {
|
|||
|
||||
private Long regionId;
|
||||
|
||||
/**
|
||||
* 区域ID路径(非数据库字段,用于级联选择器回显)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<Long> regionIds;
|
||||
|
||||
private Long schoolId;
|
||||
|
||||
private Long schoolGradeId;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor;
|
|||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
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.mapper.PgMemberMapper;
|
||||
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.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -31,6 +35,7 @@ public class PgMemberServiceImpl implements IPgMemberService {
|
|||
|
||||
private final PgMemberMapper baseMapper;
|
||||
private final PgStudentMapper studentMapper;
|
||||
private final PgRegionMapper regionMapper;
|
||||
|
||||
private static final String DEFAULT_PASSWORD = "123456";
|
||||
|
||||
|
|
@ -48,7 +53,29 @@ public class PgMemberServiceImpl implements IPgMemberService {
|
|||
|
||||
@Override
|
||||
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
|
||||
|
|
|
|||
|
|
@ -87,6 +87,14 @@ public class PgSchoolController extends BaseController {
|
|||
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;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
|
@ -33,4 +34,10 @@ public class PgSchoolClass implements Serializable {
|
|||
private Long createBy;
|
||||
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 班级名称(非数据库字段,用于前端显示)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String className;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,4 +50,9 @@ public interface IPgSchoolService {
|
|||
* 删除年级下的班级
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@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) {
|
||||
LambdaQueryWrapper<PgSchool> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.like(StrUtil.isNotBlank(school.getSchoolName()), PgSchool::getSchoolName, school.getSchoolName());
|
||||
|
|
|
|||
|
|
@ -1,17 +1,30 @@
|
|||
package org.dromara.pangu.student.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.pangu.school.domain.PgSchool;
|
||||
import org.dromara.pangu.school.domain.vo.SchoolTreeNode;
|
||||
import org.dromara.pangu.school.service.IPgSchoolService;
|
||||
import org.dromara.pangu.student.domain.PgStudent;
|
||||
import org.dromara.pangu.student.domain.dto.StudentImportDto;
|
||||
import org.dromara.pangu.student.domain.vo.StudentVo;
|
||||
import org.dromara.pangu.student.service.IPgStudentService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 学生管理
|
||||
|
|
@ -25,16 +38,25 @@ import org.springframework.web.bind.annotation.*;
|
|||
public class PgStudentController extends BaseController {
|
||||
|
||||
private final IPgStudentService studentService;
|
||||
private final IPgSchoolService schoolService;
|
||||
|
||||
@SaCheckPermission("business:student:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<PgStudent> list(PgStudent student, PageQuery pageQuery) {
|
||||
public TableDataInfo<StudentVo> list(PgStudent student, PageQuery pageQuery) {
|
||||
return studentService.selectPageList(student, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学校树结构(用于学生管理左侧筛选)
|
||||
*/
|
||||
@GetMapping("/schoolTree")
|
||||
public R<List<SchoolTreeNode>> schoolTree() {
|
||||
return R.ok(schoolService.selectSchoolTree(new PgSchool()));
|
||||
}
|
||||
|
||||
@SaCheckPermission("business:student:query")
|
||||
@GetMapping("/{studentId}")
|
||||
public R<PgStudent> getInfo(@PathVariable Long studentId) {
|
||||
public R<StudentVo> getInfo(@PathVariable Long studentId) {
|
||||
return R.ok(studentService.selectById(studentId));
|
||||
}
|
||||
|
||||
|
|
@ -58,4 +80,67 @@ public class PgStudentController extends BaseController {
|
|||
public R<Void> remove(@PathVariable Long[] 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<List<StudentVo>> listByMemberId(@PathVariable Long memberId) {
|
||||
return R.ok(studentService.selectByMemberId(memberId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载学生导入模板
|
||||
*/
|
||||
@GetMapping("/template")
|
||||
public void downloadTemplate(HttpServletResponse response) {
|
||||
// 创建示例数据,方便用户参考填写
|
||||
List<StudentImportDto> sampleData = new ArrayList<>();
|
||||
StudentImportDto sample = new StudentImportDto();
|
||||
sample.setStudentName("张三");
|
||||
sample.setStudentNo("2026001");
|
||||
sample.setMemberPhone("13800138000");
|
||||
sample.setRegionPath("湖北省-武汉市-硚口区");
|
||||
sample.setSchoolName("武汉市第二中学");
|
||||
sample.setGradeName("高一");
|
||||
sample.setClassName("1班");
|
||||
sample.setGender("男");
|
||||
sample.setBirthday("2010-01-15");
|
||||
sampleData.add(sample);
|
||||
|
||||
ExcelUtil.exportExcel(sampleData, "学生导入模板", StudentImportDto.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入学生
|
||||
*/
|
||||
@SaCheckPermission("business:student:import")
|
||||
@Log(title = "学生管理", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/import")
|
||||
public R<Map<String, Object>> importData(MultipartFile file) throws Exception {
|
||||
// 解析 Excel 数据
|
||||
List<StudentImportDto> dataList = ExcelUtil.importExcel(file.getInputStream(), StudentImportDto.class);
|
||||
|
||||
// 调用服务层处理导入
|
||||
Map<String, Object> result = studentService.importStudents(dataList);
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@ public class PgStudent extends BaseEntity {
|
|||
|
||||
private Long schoolClassId;
|
||||
|
||||
private Long subjectId;
|
||||
|
||||
private Long memberId;
|
||||
|
||||
private String status;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package org.dromara.pangu.student.domain.dto;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 学生导入数据传输对象
|
||||
*
|
||||
* @author pangu
|
||||
*/
|
||||
@Data
|
||||
@HeadRowHeight(25)
|
||||
public class StudentImportDto {
|
||||
|
||||
@ExcelProperty(value = "姓名", index = 0)
|
||||
@ColumnWidth(15)
|
||||
private String studentName;
|
||||
|
||||
@ExcelProperty(value = "学号", index = 1)
|
||||
@ColumnWidth(20)
|
||||
private String studentNo;
|
||||
|
||||
@ExcelProperty(value = "会员手机号", index = 2)
|
||||
@ColumnWidth(15)
|
||||
private String memberPhone;
|
||||
|
||||
@ExcelProperty(value = "区域", index = 3)
|
||||
@ColumnWidth(25)
|
||||
private String regionPath;
|
||||
|
||||
@ExcelProperty(value = "学校", index = 4)
|
||||
@ColumnWidth(25)
|
||||
private String schoolName;
|
||||
|
||||
@ExcelProperty(value = "年级", index = 5)
|
||||
@ColumnWidth(12)
|
||||
private String gradeName;
|
||||
|
||||
@ExcelProperty(value = "班级", index = 6)
|
||||
@ColumnWidth(10)
|
||||
private String className;
|
||||
|
||||
@ExcelProperty(value = "性别", index = 7)
|
||||
@ColumnWidth(8)
|
||||
private String gender;
|
||||
|
||||
@ExcelProperty(value = "出生日期", index = 8)
|
||||
@ColumnWidth(12)
|
||||
private String birthday;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package org.dromara.pangu.student.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 学生视图对象(包含关联数据)
|
||||
*
|
||||
* @author pangu
|
||||
*/
|
||||
@Data
|
||||
public class StudentVo {
|
||||
|
||||
private Long studentId;
|
||||
|
||||
private String studentName;
|
||||
|
||||
private String studentNo;
|
||||
|
||||
/**
|
||||
* 性别(0未知 1男 2女)
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
private Date birthday;
|
||||
|
||||
private Long regionId;
|
||||
|
||||
private String regionPath;
|
||||
|
||||
private Long schoolId;
|
||||
|
||||
/**
|
||||
* 学校名称
|
||||
*/
|
||||
private String schoolName;
|
||||
|
||||
private Long schoolGradeId;
|
||||
|
||||
/**
|
||||
* 年级名称
|
||||
*/
|
||||
private String gradeName;
|
||||
|
||||
private Long schoolClassId;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
private String className;
|
||||
|
||||
private Long memberId;
|
||||
|
||||
/**
|
||||
* 会员昵称
|
||||
*/
|
||||
private String memberNickname;
|
||||
|
||||
/**
|
||||
* 会员手机号
|
||||
*/
|
||||
private String memberPhone;
|
||||
|
||||
private String status;
|
||||
|
||||
private Date createTime;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -3,8 +3,11 @@ package org.dromara.pangu.student.service;
|
|||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pangu.student.domain.PgStudent;
|
||||
import org.dromara.pangu.student.domain.dto.StudentImportDto;
|
||||
import org.dromara.pangu.student.domain.vo.StudentVo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 学生 Service 接口
|
||||
|
|
@ -12,10 +15,42 @@ import java.util.List;
|
|||
* @author pangu
|
||||
*/
|
||||
public interface IPgStudentService {
|
||||
TableDataInfo<PgStudent> selectPageList(PgStudent student, PageQuery pageQuery);
|
||||
TableDataInfo<StudentVo> selectPageList(PgStudent student, PageQuery pageQuery);
|
||||
List<PgStudent> selectList(PgStudent student);
|
||||
PgStudent selectById(Long studentId);
|
||||
StudentVo selectById(Long studentId);
|
||||
int insert(PgStudent student);
|
||||
int update(PgStudent student);
|
||||
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<StudentVo> selectByMemberId(Long memberId);
|
||||
|
||||
/**
|
||||
* 批量绑定学生到会员
|
||||
*/
|
||||
int bindStudentsToMember(Long memberId, List<Long> studentIds);
|
||||
|
||||
/**
|
||||
* 解绑学生
|
||||
*/
|
||||
int unbindStudent(Long studentId);
|
||||
|
||||
/**
|
||||
* 批量导入学生
|
||||
* @param dataList Excel 导入的数据列表
|
||||
* @return 导入结果 {successCount, failCount, failList}
|
||||
*/
|
||||
Map<String, Object> importStudents(List<StudentImportDto> dataList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,69 @@
|
|||
package org.dromara.pangu.student.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pangu.base.domain.PgClass;
|
||||
import org.dromara.pangu.base.domain.PgGrade;
|
||||
import org.dromara.pangu.base.mapper.PgClassMapper;
|
||||
import org.dromara.pangu.base.mapper.PgGradeMapper;
|
||||
import org.dromara.pangu.member.domain.PgMember;
|
||||
import org.dromara.pangu.member.mapper.PgMemberMapper;
|
||||
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.student.domain.PgStudent;
|
||||
import org.dromara.pangu.student.domain.dto.StudentImportDto;
|
||||
import org.dromara.pangu.student.domain.vo.StudentVo;
|
||||
import org.dromara.pangu.student.mapper.PgStudentMapper;
|
||||
import org.dromara.pangu.student.service.IPgStudentService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 学生 Service 实现
|
||||
*
|
||||
* @author pangu
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class PgStudentServiceImpl implements IPgStudentService {
|
||||
|
||||
private final PgStudentMapper baseMapper;
|
||||
private final PgSchoolMapper schoolMapper;
|
||||
private final PgSchoolGradeMapper schoolGradeMapper;
|
||||
private final PgSchoolClassMapper schoolClassMapper;
|
||||
private final PgGradeMapper gradeMapper;
|
||||
private final PgClassMapper classMapper;
|
||||
private final PgMemberMapper memberMapper;
|
||||
|
||||
@Override
|
||||
public TableDataInfo<PgStudent> selectPageList(PgStudent student, PageQuery pageQuery) {
|
||||
public TableDataInfo<StudentVo> selectPageList(PgStudent student, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<PgStudent> lqw = buildQueryWrapper(student);
|
||||
Page<PgStudent> page = baseMapper.selectPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(page);
|
||||
|
||||
// 转换为 VO 并填充关联数据
|
||||
List<StudentVo> voList = convertToVoList(page.getRecords());
|
||||
|
||||
Page<StudentVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
voPage.setRecords(voList);
|
||||
return TableDataInfo.build(voPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -38,8 +72,12 @@ public class PgStudentServiceImpl implements IPgStudentService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public PgStudent selectById(Long studentId) {
|
||||
return baseMapper.selectById(studentId);
|
||||
public StudentVo selectById(Long studentId) {
|
||||
PgStudent student = baseMapper.selectById(studentId);
|
||||
if (student == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToVo(student);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -61,6 +99,7 @@ public class PgStudentServiceImpl implements IPgStudentService {
|
|||
LambdaQueryWrapper<PgStudent> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.like(StrUtil.isNotBlank(student.getStudentName()), PgStudent::getStudentName, student.getStudentName());
|
||||
lqw.like(StrUtil.isNotBlank(student.getStudentNo()), PgStudent::getStudentNo, student.getStudentNo());
|
||||
lqw.eq(StrUtil.isNotBlank(student.getGender()), PgStudent::getGender, student.getGender());
|
||||
lqw.eq(student.getSchoolId() != null, PgStudent::getSchoolId, student.getSchoolId());
|
||||
lqw.eq(student.getSchoolGradeId() != null, PgStudent::getSchoolGradeId, student.getSchoolGradeId());
|
||||
lqw.eq(student.getSchoolClassId() != null, PgStudent::getSchoolClassId, student.getSchoolClassId());
|
||||
|
|
@ -68,4 +107,436 @@ public class PgStudentServiceImpl implements IPgStudentService {
|
|||
lqw.orderByDesc(PgStudent::getCreateTime);
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换为 VO 并填充关联数据
|
||||
*/
|
||||
private List<StudentVo> convertToVoList(List<PgStudent> students) {
|
||||
if (students == null || students.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 收集所有需要查询的 ID
|
||||
Set<Long> schoolIds = new HashSet<>();
|
||||
Set<Long> schoolGradeIds = new HashSet<>();
|
||||
Set<Long> schoolClassIds = new HashSet<>();
|
||||
Set<Long> memberIds = new HashSet<>();
|
||||
|
||||
for (PgStudent s : students) {
|
||||
if (s.getSchoolId() != null) schoolIds.add(s.getSchoolId());
|
||||
if (s.getSchoolGradeId() != null) schoolGradeIds.add(s.getSchoolGradeId());
|
||||
if (s.getSchoolClassId() != null) schoolClassIds.add(s.getSchoolClassId());
|
||||
if (s.getMemberId() != null) memberIds.add(s.getMemberId());
|
||||
}
|
||||
|
||||
// 批量查询关联数据
|
||||
Map<Long, PgSchool> schoolMap = schoolIds.isEmpty() ? Collections.emptyMap() :
|
||||
schoolMapper.selectByIds(schoolIds).stream()
|
||||
.collect(Collectors.toMap(PgSchool::getSchoolId, Function.identity()));
|
||||
|
||||
Map<Long, PgSchoolGrade> schoolGradeMap = schoolGradeIds.isEmpty() ? Collections.emptyMap() :
|
||||
schoolGradeMapper.selectByIds(schoolGradeIds).stream()
|
||||
.collect(Collectors.toMap(PgSchoolGrade::getId, Function.identity()));
|
||||
|
||||
Map<Long, PgSchoolClass> schoolClassMap = schoolClassIds.isEmpty() ? Collections.emptyMap() :
|
||||
schoolClassMapper.selectByIds(schoolClassIds).stream()
|
||||
.collect(Collectors.toMap(PgSchoolClass::getId, Function.identity()));
|
||||
|
||||
Map<Long, PgMember> memberMap = memberIds.isEmpty() ? Collections.emptyMap() :
|
||||
memberMapper.selectByIds(memberIds).stream()
|
||||
.collect(Collectors.toMap(PgMember::getMemberId, Function.identity()));
|
||||
|
||||
// 收集基础年级和班级 ID
|
||||
Set<Long> baseGradeIds = schoolGradeMap.values().stream()
|
||||
.map(PgSchoolGrade::getGradeId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Set<Long> baseClassIds = schoolClassMap.values().stream()
|
||||
.map(PgSchoolClass::getClassId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 查询基础年级和班级名称
|
||||
Map<Long, String> gradeNameMap = baseGradeIds.isEmpty() ? Collections.emptyMap() :
|
||||
gradeMapper.selectByIds(baseGradeIds).stream()
|
||||
.collect(Collectors.toMap(PgGrade::getGradeId, PgGrade::getGradeName));
|
||||
|
||||
Map<Long, String> classNameMap = baseClassIds.isEmpty() ? Collections.emptyMap() :
|
||||
classMapper.selectByIds(baseClassIds).stream()
|
||||
.collect(Collectors.toMap(PgClass::getClassId, PgClass::getClassName));
|
||||
|
||||
// 转换为 VO
|
||||
List<StudentVo> voList = new ArrayList<>();
|
||||
for (PgStudent s : students) {
|
||||
StudentVo vo = new StudentVo();
|
||||
BeanUtil.copyProperties(s, vo);
|
||||
|
||||
// 填充学校名称
|
||||
PgSchool school = schoolMap.get(s.getSchoolId());
|
||||
if (school != null) {
|
||||
vo.setSchoolName(school.getSchoolName());
|
||||
}
|
||||
|
||||
// 填充年级名称
|
||||
PgSchoolGrade schoolGrade = schoolGradeMap.get(s.getSchoolGradeId());
|
||||
if (schoolGrade != null && schoolGrade.getGradeId() != null) {
|
||||
vo.setGradeName(gradeNameMap.get(schoolGrade.getGradeId()));
|
||||
}
|
||||
|
||||
// 填充班级名称
|
||||
PgSchoolClass schoolClass = schoolClassMap.get(s.getSchoolClassId());
|
||||
if (schoolClass != null && schoolClass.getClassId() != null) {
|
||||
vo.setClassName(classNameMap.get(schoolClass.getClassId()));
|
||||
}
|
||||
|
||||
// 填充会员信息
|
||||
PgMember member = memberMap.get(s.getMemberId());
|
||||
if (member != null) {
|
||||
vo.setMemberNickname(member.getNickname());
|
||||
vo.setMemberPhone(member.getPhone());
|
||||
}
|
||||
|
||||
voList.add(vo);
|
||||
}
|
||||
|
||||
return voList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个转换为 VO
|
||||
*/
|
||||
private StudentVo convertToVo(PgStudent student) {
|
||||
List<StudentVo> voList = convertToVoList(Collections.singletonList(student));
|
||||
return voList.isEmpty() ? null : voList.get(0);
|
||||
}
|
||||
|
||||
@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<StudentVo> selectByMemberId(Long memberId) {
|
||||
if (memberId == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<PgStudent> students = baseMapper.selectList(
|
||||
new LambdaQueryWrapper<PgStudent>()
|
||||
.eq(PgStudent::getMemberId, memberId)
|
||||
.orderByDesc(PgStudent::getCreateTime)
|
||||
);
|
||||
// 转换为 VO,包含学校、年级、班级名称
|
||||
return convertToVoList(students);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
return baseMapper.update(null,
|
||||
new LambdaUpdateWrapper<PgStudent>()
|
||||
.eq(PgStudent::getStudentId, studentId)
|
||||
.set(PgStudent::getMemberId, null)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> importStudents(List<StudentImportDto> dataList) {
|
||||
int successCount = 0;
|
||||
List<Map<String, Object>> failList = new ArrayList<>();
|
||||
|
||||
// 预先查询所有学校,方便匹配
|
||||
List<PgSchool> allSchools = schoolMapper.selectList(new LambdaQueryWrapper<>());
|
||||
Map<String, PgSchool> schoolNameMap = allSchools.stream()
|
||||
.collect(Collectors.toMap(PgSchool::getSchoolName, Function.identity(), (a, b) -> a));
|
||||
|
||||
// 预先查询所有年级
|
||||
List<PgGrade> allGrades = gradeMapper.selectList(new LambdaQueryWrapper<>());
|
||||
Map<String, PgGrade> gradeNameMap = allGrades.stream()
|
||||
.collect(Collectors.toMap(PgGrade::getGradeName, Function.identity(), (a, b) -> a));
|
||||
|
||||
// 预先查询所有班级
|
||||
List<PgClass> allClasses = classMapper.selectList(new LambdaQueryWrapper<>());
|
||||
Map<String, PgClass> classNameMap = allClasses.stream()
|
||||
.collect(Collectors.toMap(PgClass::getClassName, Function.identity(), (a, b) -> a));
|
||||
|
||||
for (int i = 0; i < dataList.size(); i++) {
|
||||
StudentImportDto dto = dataList.get(i);
|
||||
int rowNum = i + 2; // Excel 行号从2开始(1是表头)
|
||||
|
||||
try {
|
||||
// 1. 校验必填字段
|
||||
String error = validateImportRow(dto);
|
||||
if (error != null) {
|
||||
failList.add(createFailItem(rowNum, error));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. 查找学校
|
||||
PgSchool school = schoolNameMap.get(dto.getSchoolName().trim());
|
||||
if (school == null) {
|
||||
failList.add(createFailItem(rowNum, "学校\"" + dto.getSchoolName() + "\"不存在"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 查找年级(学校年级关联表)
|
||||
PgGrade baseGrade = gradeNameMap.get(dto.getGradeName().trim());
|
||||
if (baseGrade == null) {
|
||||
failList.add(createFailItem(rowNum, "年级\"" + dto.getGradeName() + "\"不存在"));
|
||||
continue;
|
||||
}
|
||||
PgSchoolGrade schoolGrade = schoolGradeMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgSchoolGrade>()
|
||||
.eq(PgSchoolGrade::getSchoolId, school.getSchoolId())
|
||||
.eq(PgSchoolGrade::getGradeId, baseGrade.getGradeId())
|
||||
);
|
||||
if (schoolGrade == null) {
|
||||
failList.add(createFailItem(rowNum, "学校\"" + dto.getSchoolName() + "\"下没有年级\"" + dto.getGradeName() + "\""));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. 查找班级(学校班级关联表)
|
||||
PgClass baseClass = classNameMap.get(dto.getClassName().trim());
|
||||
if (baseClass == null) {
|
||||
failList.add(createFailItem(rowNum, "班级\"" + dto.getClassName() + "\"不存在"));
|
||||
continue;
|
||||
}
|
||||
PgSchoolClass schoolClass = schoolClassMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgSchoolClass>()
|
||||
.eq(PgSchoolClass::getSchoolGradeId, schoolGrade.getId())
|
||||
.eq(PgSchoolClass::getClassId, baseClass.getClassId())
|
||||
);
|
||||
if (schoolClass == null) {
|
||||
failList.add(createFailItem(rowNum, "年级\"" + dto.getGradeName() + "\"下没有班级\"" + dto.getClassName() + "\""));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. 查找或创建会员
|
||||
PgMember member = findOrCreateMember(dto.getMemberPhone().trim());
|
||||
Long memberId = member.getMemberId();
|
||||
|
||||
// 6. 教师身份校验:教师的区域/学校/年级/班级必须与学生一致
|
||||
if ("2".equals(member.getIdentityType())) {
|
||||
String teacherError = validateTeacherStudent(member, school, schoolGrade, schoolClass);
|
||||
if (teacherError != null) {
|
||||
failList.add(createFailItem(rowNum, teacherError));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 检查学号是否重复
|
||||
if (StrUtil.isNotBlank(dto.getStudentNo())) {
|
||||
PgStudent existStudent = baseMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgStudent>()
|
||||
.eq(PgStudent::getStudentNo, dto.getStudentNo().trim())
|
||||
);
|
||||
if (existStudent != null) {
|
||||
failList.add(createFailItem(rowNum, "学号\"" + dto.getStudentNo() + "\"已存在"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 创建学生
|
||||
PgStudent student = new PgStudent();
|
||||
student.setStudentName(dto.getStudentName().trim());
|
||||
student.setStudentNo(StrUtil.isNotBlank(dto.getStudentNo()) ? dto.getStudentNo().trim() : null);
|
||||
student.setGender(convertGender(dto.getGender()));
|
||||
student.setBirthday(parseBirthday(dto.getBirthday()));
|
||||
student.setRegionPath(dto.getRegionPath());
|
||||
student.setSchoolId(school.getSchoolId());
|
||||
student.setSchoolGradeId(schoolGrade.getId());
|
||||
student.setSchoolClassId(schoolClass.getId());
|
||||
student.setMemberId(memberId);
|
||||
student.setStatus("0"); // 默认正常
|
||||
|
||||
baseMapper.insert(student);
|
||||
successCount++;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("导入第{}行数据失败", rowNum, e);
|
||||
failList.add(createFailItem(rowNum, "系统错误:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("successCount", successCount);
|
||||
result.put("failCount", failList.size());
|
||||
result.put("failList", failList);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验导入行数据
|
||||
*/
|
||||
private String validateImportRow(StudentImportDto dto) {
|
||||
if (StrUtil.isBlank(dto.getStudentName())) {
|
||||
return "姓名不能为空";
|
||||
}
|
||||
if (StrUtil.isBlank(dto.getStudentNo())) {
|
||||
return "学号不能为空";
|
||||
}
|
||||
if (StrUtil.isBlank(dto.getMemberPhone())) {
|
||||
return "会员手机号不能为空";
|
||||
}
|
||||
if (!dto.getMemberPhone().matches("^1[3-9]\\d{9}$")) {
|
||||
return "会员手机号格式不正确";
|
||||
}
|
||||
if (StrUtil.isBlank(dto.getSchoolName())) {
|
||||
return "学校不能为空";
|
||||
}
|
||||
if (StrUtil.isBlank(dto.getGradeName())) {
|
||||
return "年级不能为空";
|
||||
}
|
||||
if (StrUtil.isBlank(dto.getClassName())) {
|
||||
return "班级不能为空";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建失败项
|
||||
*/
|
||||
private Map<String, Object> createFailItem(int row, String reason) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("row", row);
|
||||
item.put("reason", reason);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找或创建会员
|
||||
*/
|
||||
private PgMember findOrCreateMember(String phone) {
|
||||
// 先查找已有会员
|
||||
PgMember member = memberMapper.selectOne(
|
||||
new LambdaQueryWrapper<PgMember>()
|
||||
.eq(PgMember::getPhone, phone)
|
||||
);
|
||||
if (member != null) {
|
||||
return member;
|
||||
}
|
||||
|
||||
// 不存在则创建新会员(身份为家长,初始密码123456)
|
||||
PgMember newMember = new PgMember();
|
||||
newMember.setMemberCode(generateMemberCode()); // 生成会员编码
|
||||
newMember.setPhone(phone);
|
||||
newMember.setNickname("家长" + phone.substring(7)); // 默认昵称
|
||||
newMember.setIdentityType("1"); // 家长
|
||||
newMember.setPassword(BCrypt.hashpw("123456")); // 初始密码
|
||||
newMember.setStatus("0"); // 正常
|
||||
newMember.setRegisterSource("4"); // 批量导入
|
||||
newMember.setRegisterTime(new Date()); // 注册时间
|
||||
memberMapper.insert(newMember);
|
||||
|
||||
return newMember;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验教师与学生的归属关系
|
||||
* 教师的区域/学校/年级/班级必须与学生一致
|
||||
*/
|
||||
private String validateTeacherStudent(PgMember teacher, PgSchool studentSchool,
|
||||
PgSchoolGrade studentGrade, PgSchoolClass studentClass) {
|
||||
String teacherInfo = "教师\"" + (teacher.getNickname() != null ? teacher.getNickname() : "未知")
|
||||
+ "\"(" + teacher.getPhone() + ")";
|
||||
|
||||
// 检查教师是否设置了学校信息
|
||||
if (teacher.getSchoolId() == null || teacher.getSchoolGradeId() == null || teacher.getSchoolClassId() == null) {
|
||||
return teacherInfo + "未设置学校信息,无法绑定学生";
|
||||
}
|
||||
|
||||
// 校验区域(通过学校的区域ID间接校验)
|
||||
if (teacher.getRegionId() != null && studentSchool.getRegionId() != null) {
|
||||
if (!teacher.getRegionId().equals(studentSchool.getRegionId())) {
|
||||
return teacherInfo + "所属区域与学生不一致";
|
||||
}
|
||||
}
|
||||
|
||||
// 校验学校
|
||||
if (!teacher.getSchoolId().equals(studentSchool.getSchoolId())) {
|
||||
return teacherInfo + "所属学校与学生不一致";
|
||||
}
|
||||
|
||||
// 校验年级
|
||||
if (!teacher.getSchoolGradeId().equals(studentGrade.getId())) {
|
||||
return teacherInfo + "所属年级与学生不一致";
|
||||
}
|
||||
|
||||
// 校验班级
|
||||
if (!teacher.getSchoolClassId().equals(studentClass.getId())) {
|
||||
return teacherInfo + "所属班级与学生不一致";
|
||||
}
|
||||
|
||||
return null; // 校验通过
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会员编码(M + 年月日时分秒毫秒 + 4位随机数)
|
||||
*/
|
||||
private String generateMemberCode() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
|
||||
String dateStr = sdf.format(new Date());
|
||||
int random = (int) (Math.random() * 9000) + 1000;
|
||||
return "M" + dateStr + random;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换性别
|
||||
*/
|
||||
private String convertGender(String gender) {
|
||||
if (StrUtil.isBlank(gender)) {
|
||||
return "0"; // 未知
|
||||
}
|
||||
if ("男".equals(gender.trim())) {
|
||||
return "1";
|
||||
}
|
||||
if ("女".equals(gender.trim())) {
|
||||
return "2";
|
||||
}
|
||||
return "0"; // 未知
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析出生日期
|
||||
*/
|
||||
private Date parseBirthday(String birthday) {
|
||||
if (StrUtil.isBlank(birthday)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
return sdf.parse(birthday.trim());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@
|
|||
| 手机号 | 文本 | ✓ | 11位手机号,唯一 |
|
||||
| 昵称 | 文本 | - | 最大长度50字符 |
|
||||
| 性别 | 枚举 | - | 男/女/未知 |
|
||||
| 出生日期 | 日期 | - | - |
|
||||
| 出生日期 | 日期 | - | 格式:YYYY-MM-DD |
|
||||
| 身份类型 | 枚举 | ✓ | 家长/教师 |
|
||||
| 所属区域 | 树形选择 | 条件必填 | 教师身份必填 |
|
||||
| 所属学校 | 下拉选择 | 条件必填 | 教师身份必填 |
|
||||
|
|
@ -322,8 +322,8 @@
|
|||
|
||||
| 功能编号 | 功能名称 | 功能描述 | 优先级 |
|
||||
| ------- | ------ | ------------------- |:---:|
|
||||
| STU-001 | 学生列表查询 | 按姓名、学号、性别、手机号、学科等筛选 | P0 |
|
||||
| STU-002 | 学校树筛选 | 通过左侧学校树快速定位学生 | P0 |
|
||||
| STU-001 | 学生列表查询 | 按姓名、学号、性别、手机号等筛选 | P0 |
|
||||
| STU-002 | 学校树筛选 | 通过左侧学校树快速定位学生,点击节点时带上完整层级条件(学校+年级+班级) | P0 |
|
||||
| STU-003 | 新增学生 | 手动创建学生信息 | P0 |
|
||||
| STU-004 | 编辑学生 | 修改学生基本信息 | P0 |
|
||||
| STU-005 | 删除学生 | 软删除学生信息 | P1 |
|
||||
|
|
@ -356,7 +356,7 @@
|
|||
| 6 | 年级 | ✓ | 年级名称,需与系统数据匹配 |
|
||||
| 7 | 班级 | ✓ | 班级名称,需与系统数据匹配 |
|
||||
| 8 | 性别 | - | 男/女 |
|
||||
| 9 | 出生年月 | - | 格式:YYYY-MM |
|
||||
| 9 | 出生日期 | - | 格式:YYYY-MM-DD |
|
||||
|
||||
#### 4.3.5 数据项说明
|
||||
|
||||
|
|
@ -367,12 +367,11 @@
|
|||
| 姓名 | 文本 | ✓ | 最大长度50字符 |
|
||||
| 学号 | 文本 | - | 唯一,最大长度20字符 |
|
||||
| 性别 | 枚举 | - | 男/女/未知 |
|
||||
| 出生年月 | 日期 | - | - |
|
||||
| 出生日期 | 日期 | - | 格式:YYYY-MM-DD |
|
||||
| 所属区域 | 树形选择 | ✓ | 省-市-区三级 |
|
||||
| 所属学校 | 下拉选择 | ✓ | 依赖区域 |
|
||||
| 所属年级 | 下拉选择 | ✓ | 依赖学校 |
|
||||
| 所属班级 | 下拉选择 | ✓ | 依赖年级 |
|
||||
| 学科 | 下拉选择 | - | 学科信息 |
|
||||
| 归属用户 | 关联 | ✓ | 关联会员ID |
|
||||
| 创建时间 | 日期时间 | - | 系统自动记录 |
|
||||
|
||||
|
|
@ -826,9 +825,12 @@
|
|||
**批量导入要求:**
|
||||
| 序号 | 要求内容 |
|
||||
|:---:|--------|
|
||||
| 1 | 下载模板,模板包含姓名-必填,学号-必填,用户手机号-必填,区域-必填,学校-必填,年级-必填,班级-必填,性别-选填,出生年月-选填 |
|
||||
| 1 | 下载模板,模板包含姓名-必填,学号-必填,会员手机号-必填,区域-必填,学校-必填,年级-必填,班级-必填,性别-选填,出生日期-选填 |
|
||||
| 2 | 导入数据时,检查必填信息,并检查区域/学校/年级/班级对应的信息一致 |
|
||||
| 3 | 导入时,同时需要检查用户手机号,如果系统中存在用户手机号,那么导入的学生就挂在当前用户下;如果系统中不存在对应的用户,自动创建当前手机号的用户,身份为家长,初始密码123456,并把学生信息挂在当前用户下 |
|
||||
| 4 | **教师身份校验**:如果会员手机号对应的会员身份为"教师",需校验教师与学生的归属关系一致(区域、学校、年级、班级),不一致则导入失败并提示原因 |
|
||||
| 5 | 教师未设置学校信息时,无法绑定学生,提示"教师未设置学校信息" |
|
||||
| 6 | 重新选择文件时自动覆盖之前的文件记录,无需手动删除 |
|
||||
|
||||
**表格列定义:**
|
||||
| 字段 | 说明 |
|
||||
|
|
@ -836,12 +838,11 @@
|
|||
| 姓名 | 学生姓名 |
|
||||
| 学号 | 学生学号 |
|
||||
| 性别 | 男/女/未知 |
|
||||
| 出生年月 | 日期格式 |
|
||||
| 出生日期 | 格式:YYYY-MM-DD |
|
||||
| 地区 | 如:湖北省-武汉市-武昌区 |
|
||||
| 学校 | 学校名称 |
|
||||
| 年级 | 年级名称 |
|
||||
| 班级 | 班级名称 |
|
||||
| 学科 | 学科名称 |
|
||||
| 用户身份 | 家长/教师 |
|
||||
| 用户昵称 | 归属会员昵称 |
|
||||
| 用户手机号 | 归属会员手机号 |
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export default {
|
|||
* 是否显示 tagsView
|
||||
*/
|
||||
tagsView: true,
|
||||
|
||||
|
||||
/**
|
||||
* 显示页签图标
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -140,8 +140,8 @@
|
|||
<el-table-column prop="gradeName" label="年级" width="80" />
|
||||
<el-table-column prop="className" label="班级" width="60" />
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-popconfirm title="确定解绑该学生?" @confirm="handleRemoveStudent($index)">
|
||||
<template #default="{ row }">
|
||||
<el-popconfirm title="确定解绑该学生?" @confirm="handleRemoveStudent(row)">
|
||||
<template #reference>
|
||||
<el-button link type="danger" size="small">解绑</el-button>
|
||||
</template>
|
||||
|
|
@ -154,6 +154,9 @@
|
|||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 学生选择器 -->
|
||||
<StudentSelectDialog ref="studentSelectRef" @success="loadBoundStudents" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
|
|
@ -162,6 +165,7 @@ import { Plus } from '@element-plus/icons-vue'
|
|||
import { ElMessage } from 'element-plus'
|
||||
import { reactive, ref } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
import StudentSelectDialog from './StudentSelectDialog.vue'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
|
|
@ -169,6 +173,7 @@ const visible = ref(false)
|
|||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
const submitLoading = ref(false)
|
||||
const studentSelectRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
|
|
@ -229,10 +234,9 @@ const open = async (row) => {
|
|||
const res = await request.get(`/business/member/${row.memberId}`)
|
||||
if (res.code === 200 && res.data) {
|
||||
Object.assign(form, res.data)
|
||||
// 处理区域级联
|
||||
// 后端已返回 regionIds 数组用于级联选择器回显
|
||||
// 加载学校信息的下拉选项
|
||||
if (form.regionId) {
|
||||
// 构建regionIds数组用于级联选择器回显
|
||||
form.regionIds = [form.regionId]
|
||||
await loadSchoolList(form.regionId)
|
||||
}
|
||||
if (form.schoolId) {
|
||||
|
|
@ -241,6 +245,8 @@ const open = async (row) => {
|
|||
if (form.schoolGradeId) {
|
||||
await loadClassList(form.schoolGradeId)
|
||||
}
|
||||
// 加载已绑定的学生列表
|
||||
await loadBoundStudents()
|
||||
}
|
||||
} catch (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) => {
|
||||
try {
|
||||
const res = await request.get('/business/school/list', { params: { regionId } })
|
||||
schoolList.value = res.rows || []
|
||||
const res = await request.get('/business/school/listAll', { params: { regionId } })
|
||||
schoolList.value = res.data || []
|
||||
} catch (e) {
|
||||
schoolList.value = []
|
||||
}
|
||||
|
|
@ -299,7 +321,7 @@ const loadSchoolList = async (regionId) => {
|
|||
*/
|
||||
const loadGradeList = async (schoolId) => {
|
||||
try {
|
||||
const res = await request.get('/business/school/grades', { params: { schoolId } })
|
||||
const res = await request.get(`/business/school/${schoolId}/grades`)
|
||||
gradeList.value = res.data || []
|
||||
} catch (e) {
|
||||
gradeList.value = []
|
||||
|
|
@ -311,7 +333,7 @@ const loadGradeList = async (schoolId) => {
|
|||
*/
|
||||
const loadClassList = async (schoolGradeId) => {
|
||||
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 || []
|
||||
} catch (e) {
|
||||
classList.value = []
|
||||
|
|
@ -379,14 +401,30 @@ const handleGradeChange = () => {
|
|||
* 添加学生
|
||||
*/
|
||||
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) => {
|
||||
form.students.splice(index, 1)
|
||||
const handleRemoveStudent = async (row) => {
|
||||
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>
|
||||
|
|
@ -48,10 +48,17 @@
|
|||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
|
||||
<el-table-column prop="memberCode" label="会员编号" width="160" />
|
||||
<el-table-column prop="phone" label="手机号" width="130">
|
||||
<el-table-column prop="memberCode" label="会员编号" width="185" />
|
||||
<el-table-column prop="phone" label="手机号" width="145">
|
||||
<template #default="{ row }">
|
||||
{{ maskPhone(row.phone) }}
|
||||
<span>{{ phoneVisibleMap[row.memberId] ? row.phone : maskPhone(row.phone) }}</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:icon="phoneVisibleMap[row.memberId] ? Hide : View"
|
||||
style="margin-left: 4px"
|
||||
@click="togglePhoneVisible(row.memberId)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nickname" label="昵称" min-width="100" show-overflow-tooltip />
|
||||
|
|
@ -60,7 +67,11 @@
|
|||
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="birthday" label="出生日期" width="110" />
|
||||
<el-table-column prop="birthday" label="出生日期" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ formatBirthday(row.birthday) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="identityType" label="身份类型" width="85" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.identityType === '1' ? 'success' : 'warning'">
|
||||
|
|
@ -129,7 +140,7 @@
|
|||
* 会员管理页面
|
||||
* @author pangu
|
||||
*/
|
||||
import { Delete, Edit, Key, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { Delete, Edit, Hide, Key, Plus, Refresh, Search, View } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
|
|
@ -153,6 +164,14 @@ const queryParams = ref({
|
|||
const memberDialogRef = ref()
|
||||
const resetPwdDialogRef = ref()
|
||||
|
||||
// 手机号显示状态(按会员ID存储)
|
||||
const phoneVisibleMap = ref({})
|
||||
|
||||
// 切换手机号显示/隐藏
|
||||
const togglePhoneVisible = (memberId) => {
|
||||
phoneVisibleMap.value[memberId] = !phoneVisibleMap.value[memberId]
|
||||
}
|
||||
|
||||
// 手机号脱敏
|
||||
const maskPhone = (phone) => {
|
||||
if (!phone || phone.length !== 11) return phone
|
||||
|
|
@ -165,6 +184,22 @@ const formatRegisterSource = (source) => {
|
|||
return map[source] || source
|
||||
}
|
||||
|
||||
// 格式化出生日期(只显示年月日)
|
||||
const formatBirthday = (birthday) => {
|
||||
if (!birthday) return ''
|
||||
// 如果已经是 YYYY-MM-DD 格式,直接返回前10位
|
||||
if (typeof birthday === 'string' && birthday.length >= 10) {
|
||||
return birthday.substring(0, 10)
|
||||
}
|
||||
// 如果是 Date 对象或时间戳
|
||||
const d = new Date(birthday)
|
||||
if (isNaN(d.getTime())) return birthday
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 获取会员列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<el-dialog
|
||||
v-model="visible"
|
||||
title="批量导入学生"
|
||||
width="500px"
|
||||
width="550px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
|
|
@ -16,34 +16,42 @@
|
|||
<div style="line-height: 1.8">
|
||||
1. 请先下载导入模板,按模板格式填写数据<br>
|
||||
2. 支持 xlsx、xls 格式文件,单次最多导入500条<br>
|
||||
3. 必填字段:姓名、学校、年级、班级
|
||||
3. 必填字段:姓名、学号、会员手机号、区域、学校、年级、班级<br>
|
||||
4. 若会员手机号已存在则挂载到已有会员,否则自动创建家长账号
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<el-button type="primary" :icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
|
||||
<el-button type="primary" :icon="Download" @click="handleDownloadTemplate" :loading="downloadLoading">
|
||||
下载模板
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:http-request="customUpload"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="handleSuccess"
|
||||
:on-error="handleError"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><Upload /></el-icon>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 xlsx/xls 文件</div>
|
||||
<div class="el-upload__tip">只能上传 xlsx/xls 文件,文件大小不超过 10MB</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<!-- 上传进度 -->
|
||||
<div v-if="uploading" style="margin-top: 16px;">
|
||||
<el-progress :percentage="uploadProgress" :status="uploadProgress === 100 ? 'success' : ''" />
|
||||
<div style="margin-top: 8px; color: #909399; font-size: 12px;">
|
||||
{{ uploadProgress === 100 ? '上传完成,正在处理数据...' : '正在上传...' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入结果 -->
|
||||
<div v-if="importResult" style="margin-top: 16px;">
|
||||
<el-alert
|
||||
|
|
@ -52,15 +60,16 @@
|
|||
:closable="false"
|
||||
/>
|
||||
<div v-if="importResult.failList && importResult.failList.length > 0" style="margin-top: 12px;">
|
||||
<div style="margin-bottom: 8px; font-size: 14px; color: #606266;">失败明细:</div>
|
||||
<el-table :data="importResult.failList" border size="small" max-height="200">
|
||||
<el-table-column prop="row" label="行号" width="80" />
|
||||
<el-table-column prop="reason" label="失败原因" min-width="200" />
|
||||
<el-table-column prop="row" label="行号" width="80" align="center" />
|
||||
<el-table-column prop="reason" label="失败原因" min-width="200" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
|
@ -70,44 +79,56 @@
|
|||
* 学生批量导入弹窗
|
||||
* @author pangu
|
||||
*/
|
||||
import { Download, Upload } from '@element-plus/icons-vue'
|
||||
import { Download, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const uploadRef = ref(null)
|
||||
const importResult = ref(null)
|
||||
|
||||
// 上传地址
|
||||
const uploadUrl = '/business/student/import'
|
||||
|
||||
// 上传请求头
|
||||
const uploadHeaders = computed(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { Authorization: 'Bearer ' + token } : {}
|
||||
})
|
||||
const downloadLoading = ref(false)
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
|
||||
// 下载模板
|
||||
const handleDownloadTemplate = () => {
|
||||
// 实际应该调用后端接口下载模板
|
||||
ElMessage.info('模板下载功能需要对接后端下载接口')
|
||||
const handleDownloadTemplate = async () => {
|
||||
downloadLoading.value = true
|
||||
try {
|
||||
// 使用 axios 下载文件(走 Vite 代理)
|
||||
const response = await request({
|
||||
url: '/business/student/template',
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
// response 就是 blob 数据
|
||||
const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '学生导入模板.xlsx'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('模板下载成功')
|
||||
} catch (e) {
|
||||
console.error('下载模板失败:', e)
|
||||
ElMessage.error('下载模板失败,请重试')
|
||||
} finally {
|
||||
downloadLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 上传前校验
|
||||
const beforeUpload = (file) => {
|
||||
// 清空之前的文件列表,实现"选择即覆盖"
|
||||
uploadRef.value?.clearFiles()
|
||||
|
||||
const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls')
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件')
|
||||
|
|
@ -122,25 +143,73 @@ const beforeUpload = (file) => {
|
|||
return true
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
const handleSuccess = (response) => {
|
||||
if (response.code === 200) {
|
||||
importResult.value = response.data
|
||||
if (response.data.failCount === 0) {
|
||||
ElMessage.success('导入成功')
|
||||
emit('success')
|
||||
// 自定义上传(使用 request 走 Vite 代理,自动携带 token)
|
||||
const customUpload = async ({ file, onProgress, onSuccess, onError }) => {
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/business/student/import',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||
uploadProgress.value = percent
|
||||
onProgress({ percent })
|
||||
}
|
||||
})
|
||||
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
|
||||
// response 是 axios 解析后的数据
|
||||
if (response.code === 200) {
|
||||
importResult.value = response.data
|
||||
if (response.data.failCount === 0) {
|
||||
ElMessage.success(`导入成功,共导入 ${response.data.successCount} 条数据`)
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.warning('部分数据导入失败,请查看失败原因')
|
||||
}
|
||||
onSuccess(response)
|
||||
} else {
|
||||
ElMessage.warning('部分数据导入失败,请查看失败原因')
|
||||
ElMessage.error(response.msg || '导入失败')
|
||||
onError(new Error(response.msg || '导入失败'))
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(response.msg || '导入失败')
|
||||
} catch (error) {
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error('文件上传失败,请重试')
|
||||
onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
const handleError = () => {
|
||||
ElMessage.error('文件上传失败,请重试')
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
// 清理状态
|
||||
importResult.value = null
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
importResult.value = null
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择归属用户"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
>
|
||||
<!-- 搜索区域 -->
|
||||
<el-form :inline="true" style="margin-bottom: 16px">
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="queryParams.nickname" placeholder="请输入昵称" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">搜索</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
max-height="400"
|
||||
>
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column prop="nickname" label="昵称" width="120" />
|
||||
<el-table-column prop="identityType" label="身份" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.identityType === '1' ? 'success' : 'primary'" size="small">
|
||||
{{ row.identityType === '1' ? '家长' : '教师' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="gender" label="性别" width="60" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="注册时间" width="160" />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!selectedRow">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 会员选择弹窗
|
||||
* @author pangu
|
||||
*/
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const selectedRow = ref(null)
|
||||
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
phone: '',
|
||||
nickname: ''
|
||||
})
|
||||
|
||||
// 获取会员列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await request.get('/business/member/list', { params: queryParams.value })
|
||||
if (res.code === 200) {
|
||||
tableData.value = res.rows || []
|
||||
total.value = res.total || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取会员列表失败:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const resetQuery = () => {
|
||||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
phone: '',
|
||||
nickname: ''
|
||||
}
|
||||
getList()
|
||||
}
|
||||
|
||||
// 选中行变化
|
||||
const handleCurrentChange = (row) => {
|
||||
selectedRow.value = row
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (!selectedRow.value) {
|
||||
ElMessage.warning('请选择一个会员')
|
||||
return
|
||||
}
|
||||
emit('select', selectedRow.value)
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
selectedRow.value = null
|
||||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
phone: '',
|
||||
nickname: ''
|
||||
}
|
||||
getList()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
|
@ -5,19 +5,19 @@
|
|||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@open="handleOpen"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入学生姓名" maxlength="20" />
|
||||
<el-form-item label="姓名" prop="studentName">
|
||||
<el-input v-model="form.studentName" placeholder="请输入学生姓名" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="学号" prop="studentNo">
|
||||
<el-input v-model="form.studentNo" placeholder="请输入学号" maxlength="30" />
|
||||
<el-input v-model="form.studentNo" placeholder="请输入学号(选填)" maxlength="30" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<el-radio-group v-model="form.gender">
|
||||
|
|
@ -29,39 +29,32 @@
|
|||
<el-form-item label="出生日期" prop="birthday">
|
||||
<el-date-picker
|
||||
v-model="form.birthday"
|
||||
type="month"
|
||||
placeholder="请选择出生年月"
|
||||
format="YYYY-MM"
|
||||
value-format="YYYY-MM"
|
||||
type="date"
|
||||
placeholder="请选择出生日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="学校信息" prop="schoolPath" required>
|
||||
<el-form-item label="学校信息" prop="schoolClassId" required>
|
||||
<el-cascader
|
||||
v-model="form.schoolPath"
|
||||
:options="schoolTree"
|
||||
:options="schoolTreeData"
|
||||
:props="{
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
checkStrictly: false
|
||||
}"
|
||||
placeholder="请选择学校/年级/班级"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleSchoolChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="学科" prop="subject">
|
||||
<el-select v-model="form.subject" placeholder="请选择学科" clearable style="width: 100%">
|
||||
<el-option v-for="item in subjectList" :key="item.id" :label="item.name" :value="item.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="归属用户" prop="userId">
|
||||
<el-input v-model="form.userNickname" placeholder="请输入用户昵称搜索" readonly>
|
||||
<template #append>
|
||||
<el-button @click="handleSelectUser">选择</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-form-item label="归属用户">
|
||||
<span style="color: #F56C6C;">{{ form.memberDisplay || '暂无归属用户' }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
@ -76,95 +69,123 @@
|
|||
* 学生新增/编辑弹窗
|
||||
* @author pangu
|
||||
*/
|
||||
import { addStudent, getStudent, updateStudent } from '@/api/pangu/student'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
studentId: {
|
||||
type: [Number, null],
|
||||
default: null
|
||||
},
|
||||
schoolTree: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
subjectList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!props.studentId)
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
const formLoading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 学校树数据
|
||||
const schoolTreeData = ref([])
|
||||
|
||||
const initialForm = {
|
||||
id: null,
|
||||
name: '',
|
||||
studentId: null,
|
||||
studentName: '',
|
||||
studentNo: '',
|
||||
gender: '1',
|
||||
birthday: '',
|
||||
schoolPath: [],
|
||||
subject: '',
|
||||
userId: null,
|
||||
userNickname: ''
|
||||
schoolId: null,
|
||||
schoolGradeId: null,
|
||||
schoolClassId: null,
|
||||
memberId: null,
|
||||
memberDisplay: ''
|
||||
}
|
||||
|
||||
const form = reactive({ ...initialForm })
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
studentName: [
|
||||
{ required: true, message: '请输入学生姓名', trigger: 'blur' }
|
||||
],
|
||||
schoolPath: [
|
||||
schoolClassId: [
|
||||
{ required: true, message: '请选择学校/年级/班级', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 弹窗打开时
|
||||
const handleOpen = async () => {
|
||||
Object.assign(form, initialForm)
|
||||
formRef.value?.clearValidate()
|
||||
// 获取学校树数据
|
||||
const getSchoolTree = async () => {
|
||||
try {
|
||||
const res = await request.get('/business/student/schoolTree')
|
||||
if (res.code === 200) {
|
||||
schoolTreeData.value = res.data || []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取学校树失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (props.studentId) {
|
||||
// 格式化出生日期
|
||||
const formatBirthday = (date) => {
|
||||
if (!date) return ''
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 学校选择变化
|
||||
const handleSchoolChange = (value) => {
|
||||
if (value && value.length === 3) {
|
||||
form.schoolId = value[0]
|
||||
form.schoolGradeId = value[1]
|
||||
form.schoolClassId = value[2]
|
||||
} else {
|
||||
form.schoolId = null
|
||||
form.schoolGradeId = null
|
||||
form.schoolClassId = null
|
||||
}
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const open = async (row = null) => {
|
||||
visible.value = true
|
||||
isEdit.value = !!row
|
||||
formLoading.value = true
|
||||
|
||||
// 重置表单
|
||||
Object.assign(form, initialForm)
|
||||
form.schoolPath = []
|
||||
formRef.value?.clearValidate()
|
||||
|
||||
// 获取基础数据
|
||||
await getSchoolTree()
|
||||
|
||||
// 编辑模式:加载学生详情
|
||||
if (row) {
|
||||
try {
|
||||
const res = await getStudent(props.studentId)
|
||||
if (res.data) {
|
||||
const res = await request.get(`/business/student/${row.studentId}`)
|
||||
if (res.code === 200 && res.data) {
|
||||
const data = res.data
|
||||
form.id = data.id
|
||||
form.name = data.name
|
||||
form.studentId = data.studentId
|
||||
form.studentName = data.studentName
|
||||
form.studentNo = data.studentNo
|
||||
form.gender = data.gender
|
||||
form.birthday = data.birthday
|
||||
form.subject = data.subject
|
||||
form.userId = data.userId
|
||||
form.userNickname = data.userNickname
|
||||
// 需要根据实际数据结构构建 schoolPath
|
||||
form.schoolPath = [data.schoolId]
|
||||
form.gender = data.gender || '0'
|
||||
form.birthday = data.birthday ? formatBirthday(data.birthday) : ''
|
||||
form.memberId = data.memberId
|
||||
form.memberDisplay = data.memberNickname ? `${data.memberNickname}(${data.memberPhone || ''})` : ''
|
||||
|
||||
// 构建学校路径用于级联选择器回显
|
||||
if (data.schoolId && data.schoolGradeId && data.schoolClassId) {
|
||||
form.schoolPath = [data.schoolId, data.schoolGradeId, data.schoolClassId]
|
||||
form.schoolId = data.schoolId
|
||||
form.schoolGradeId = data.schoolGradeId
|
||||
form.schoolClassId = data.schoolClassId
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取学生详情失败:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选择用户
|
||||
const handleSelectUser = () => {
|
||||
// 简化处理,实际应该弹出用户选择器
|
||||
ElMessage.info('用户选择功能需要对接会员管理模块')
|
||||
|
||||
formLoading.value = false
|
||||
}
|
||||
|
||||
// 提交
|
||||
|
|
@ -174,22 +195,32 @@ const handleSubmit = async () => {
|
|||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
// 校验学校信息
|
||||
if (!form.schoolClassId) {
|
||||
ElMessage.warning('请选择完整的学校/年级/班级信息')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const submitData = {
|
||||
...form,
|
||||
schoolId: form.schoolPath[0],
|
||||
gradeId: form.schoolPath[1],
|
||||
classId: form.schoolPath[2]
|
||||
studentId: form.studentId,
|
||||
studentName: form.studentName,
|
||||
studentNo: form.studentNo,
|
||||
gender: form.gender,
|
||||
birthday: form.birthday,
|
||||
schoolId: form.schoolId,
|
||||
schoolGradeId: form.schoolGradeId,
|
||||
schoolClassId: form.schoolClassId,
|
||||
memberId: form.memberId
|
||||
}
|
||||
delete submitData.schoolPath
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateStudent(submitData)
|
||||
await request.put('/business/student', submitData)
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await addStudent(submitData)
|
||||
await request.post('/business/student', submitData)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
visible.value = false
|
||||
|
|
@ -200,4 +231,6 @@ const handleSubmit = async () => {
|
|||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="schoolTree"
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
node-key="id"
|
||||
highlight-current
|
||||
:filter-node-method="filterNode"
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<el-card shadow="never" class="search-wrapper">
|
||||
<el-form :model="queryParams" :inline="true">
|
||||
<el-form-item label="学生姓名">
|
||||
<el-input v-model="queryParams.name" placeholder="请输入学生姓名" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
||||
<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" />
|
||||
|
|
@ -59,18 +59,27 @@
|
|||
|
||||
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
|
||||
<el-table-column prop="studentNo" label="学号" width="130" />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="studentName" label="姓名" width="100" />
|
||||
<el-table-column prop="gender" label="性别" width="60" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="birthday" label="出生年月" width="100" />
|
||||
<el-table-column prop="birthday" label="出生日期" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ row.birthday ? formatDate(row.birthday) : '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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="80" />
|
||||
<el-table-column prop="subject" label="学科" width="80" />
|
||||
<el-table-column prop="userNickname" label="归属用户" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="memberNickname" label="归属用户" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.memberNickname">{{ row.memberNickname }}</span>
|
||||
<span v-else-if="row.memberPhone">{{ row.memberPhone }}</span>
|
||||
<span v-else style="color: #909399">未绑定</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
|
|
@ -129,12 +138,12 @@ const total = ref(0)
|
|||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
name: '',
|
||||
studentName: '',
|
||||
studentNo: '',
|
||||
gender: '',
|
||||
schoolId: '',
|
||||
gradeId: '',
|
||||
classId: ''
|
||||
schoolId: null,
|
||||
schoolGradeId: null,
|
||||
schoolClassId: null
|
||||
})
|
||||
|
||||
// 弹窗引用
|
||||
|
|
@ -149,7 +158,7 @@ watch(treeFilterText, (val) => {
|
|||
// 树节点过滤
|
||||
const filterNode = (value, data) => {
|
||||
if (!value) return true
|
||||
return data.label.includes(value)
|
||||
return data.name?.includes(value)
|
||||
}
|
||||
|
||||
// 获取学校树
|
||||
|
|
@ -176,16 +185,22 @@ const getList = async () => {
|
|||
|
||||
// 树节点点击
|
||||
const handleNodeClick = (data) => {
|
||||
// 根据节点层级设置筛选条件
|
||||
// 根据节点层级设置筛选条件,子节点需要带上父级条件
|
||||
if (data.type === 'school') {
|
||||
// 点击学校:只筛选学校
|
||||
queryParams.value.schoolId = data.id
|
||||
queryParams.value.gradeId = ''
|
||||
queryParams.value.classId = ''
|
||||
queryParams.value.schoolGradeId = null
|
||||
queryParams.value.schoolClassId = null
|
||||
} else if (data.type === 'grade') {
|
||||
queryParams.value.gradeId = data.id
|
||||
queryParams.value.classId = ''
|
||||
// 点击年级:筛选学校 + 年级
|
||||
queryParams.value.schoolId = data.schoolId
|
||||
queryParams.value.schoolGradeId = data.id
|
||||
queryParams.value.schoolClassId = null
|
||||
} else if (data.type === 'class') {
|
||||
queryParams.value.classId = data.id
|
||||
// 点击班级:筛选学校 + 年级 + 班级
|
||||
queryParams.value.schoolId = data.schoolId
|
||||
queryParams.value.schoolGradeId = data.schoolGradeId
|
||||
queryParams.value.schoolClassId = data.id
|
||||
}
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
|
|
@ -202,12 +217,12 @@ const resetQuery = () => {
|
|||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
name: '',
|
||||
studentName: '',
|
||||
studentNo: '',
|
||||
gender: '',
|
||||
schoolId: '',
|
||||
gradeId: '',
|
||||
classId: ''
|
||||
schoolId: null,
|
||||
schoolGradeId: null,
|
||||
schoolClassId: null
|
||||
}
|
||||
treeRef.value?.setCurrentKey(null)
|
||||
getList()
|
||||
|
|
@ -228,14 +243,24 @@ const handleImport = () => {
|
|||
importDialogRef.value?.open()
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确定要删除学生"${row.name}"吗?`, '提示', {
|
||||
ElMessageBox.confirm(`确定要删除学生"${row.studentName}"吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await request.delete(`/business/student/${row.id}`)
|
||||
const res = await request.delete(`/business/student/${row.studentId}`)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
|
|
|
|||
Loading…
Reference in New Issue