0823lxm
This commit is contained in:
1 parent
0db0a99c85
commit
8dc6aab1a6
31 files changed
+4859
No files matched your search
+80
@@ -0,0 +1,80 @@
|
||||
package org.dromara.system.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
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.system.domain.bo.SysTeacherBo;
|
||||
import org.dromara.system.domain.vo.SysTeacherVo;
|
||||
import org.dromara.system.domain.vo.SysDeptTreeVo;
|
||||
import org.dromara.system.service.ISysTeacherService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 教师信息管理
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/system/teacher")
|
||||
public class SysTeacherController extends BaseController {
|
||||
|
||||
private final ISysTeacherService sysTeacherService;
|
||||
|
||||
/**
|
||||
* 查询教师信息列表
|
||||
*/
|
||||
@SaCheckPermission("system:teacher:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<SysTeacherVo> list(SysTeacherBo bo, PageQuery pageQuery) {
|
||||
return sysTeacherService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据姓名搜索教师
|
||||
*/
|
||||
@SaCheckPermission("system:teacher:list")
|
||||
@GetMapping("/search")
|
||||
public R<List<SysTeacherVo>> searchByName(@RequestParam String name,
|
||||
@RequestParam(defaultValue = "10") Integer limit) {
|
||||
List<SysTeacherVo> list = sysTeacherService.searchByName(name, limit);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门树形结构
|
||||
*/
|
||||
@SaCheckPermission("system:teacher:list")
|
||||
@GetMapping("/deptTree")
|
||||
public R<List<SysDeptTreeVo>> getDeptTreeData() {
|
||||
List<SysDeptTreeVo> tree = sysTeacherService.getDeptTreeData();
|
||||
return R.ok(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门ID获取教师列表
|
||||
*/
|
||||
@SaCheckPermission("system:teacher:list")
|
||||
@GetMapping("/dept/{deptId}")
|
||||
public R<List<SysTeacherVo>> getTeachersByDeptId(@PathVariable Long deptId) {
|
||||
List<SysTeacherVo> list = sysTeacherService.getTeachersByDeptId(deptId);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取教师详细信息
|
||||
*/
|
||||
@SaCheckPermission("system:teacher:query")
|
||||
@GetMapping("/{userId}")
|
||||
public R<SysTeacherVo> getInfo(@PathVariable Long userId) {
|
||||
return R.ok(sysTeacherService.queryById(userId));
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package org.dromara.system.domain.bo;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 教师信息业务对象
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SysTeacherBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户账号
|
||||
*/
|
||||
@Size(min = 0, max = 30, message = "用户账号长度不能超过{max}个字符")
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 用户昵称(教师姓名)
|
||||
*/
|
||||
@NotBlank(message = "教师姓名不能为空")
|
||||
@Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符")
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
@Size(min = 0, max = 11, message = "手机号码长度不能超过{max}个字符")
|
||||
private String phonenumber;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
@Size(min = 0, max = 50, message = "用户邮箱长度不能超过{max}个字符")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*/
|
||||
private String sex;
|
||||
|
||||
/**
|
||||
* 帐号状态(0正常 1停用)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 查询关键词(支持姓名、账号、手机号搜索)
|
||||
*/
|
||||
private String searchKeyword;
|
||||
|
||||
/**
|
||||
* 查询限制数量
|
||||
*/
|
||||
private Integer limit;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package org.dromara.system.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 部门树形结构视图对象
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Data
|
||||
public class SysDeptTreeVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 父部门ID
|
||||
*/
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private String label;
|
||||
|
||||
/**
|
||||
* 显示顺序
|
||||
*/
|
||||
private Integer orderNum;
|
||||
|
||||
/**
|
||||
* 子部门
|
||||
*/
|
||||
private List<SysDeptTreeVo> children;
|
||||
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package org.dromara.system.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 教师信息视图对象
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Data
|
||||
public class SysTeacherVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户账号
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 用户昵称(教师姓名)
|
||||
*/
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String phonenumber;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*/
|
||||
private String sex;
|
||||
|
||||
/**
|
||||
* 帐号状态(0正常 1停用)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package org.dromara.system.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.domain.vo.SysTeacherVo;
|
||||
import org.dromara.system.domain.vo.SysDeptTreeVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 教师信息Mapper接口
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
public interface SysTeacherMapper extends BaseMapperPlus<SysUser, SysTeacherVo> {
|
||||
|
||||
/**
|
||||
* 分页查询教师信息列表
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param wrapper 查询条件
|
||||
* @return 教师信息分页列表
|
||||
*/
|
||||
@Select("""
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.user_name,
|
||||
u.nick_name,
|
||||
u.phonenumber,
|
||||
u.email,
|
||||
u.dept_id,
|
||||
d.dept_name,
|
||||
u.sex,
|
||||
u.status,
|
||||
u.create_time
|
||||
FROM sys_user u
|
||||
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
|
||||
WHERE u.del_flag = '0'
|
||||
AND u.account_type = '1'
|
||||
${ew.customSqlSegment}
|
||||
ORDER BY u.create_time DESC
|
||||
""")
|
||||
Page<SysTeacherVo> selectPageTeacherList(@Param("page") Page<SysTeacherVo> page, @Param(Constants.WRAPPER) Wrapper<SysUser> wrapper);
|
||||
|
||||
/**
|
||||
* 查询教师信息列表
|
||||
*
|
||||
* @param wrapper 查询条件
|
||||
* @return 教师信息列表
|
||||
*/
|
||||
@Select("""
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.user_name,
|
||||
u.nick_name,
|
||||
u.phonenumber,
|
||||
u.email,
|
||||
u.dept_id,
|
||||
d.dept_name,
|
||||
u.sex,
|
||||
u.status,
|
||||
u.create_time
|
||||
FROM sys_user u
|
||||
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
|
||||
WHERE u.del_flag = '0'
|
||||
AND u.account_type = '1'
|
||||
${ew.customSqlSegment}
|
||||
ORDER BY u.create_time DESC
|
||||
""")
|
||||
List<SysTeacherVo> selectTeacherList(@Param(Constants.WRAPPER) Wrapper<SysUser> wrapper);
|
||||
|
||||
/**
|
||||
* 根据姓名搜索教师(模糊查询)
|
||||
*
|
||||
* @param name 教师姓名关键字
|
||||
* @param limit 查询限制数量
|
||||
* @return 教师信息列表
|
||||
*/
|
||||
@Select("""
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.user_name,
|
||||
u.nick_name,
|
||||
u.phonenumber,
|
||||
u.email,
|
||||
u.dept_id,
|
||||
d.dept_name,
|
||||
u.sex,
|
||||
u.status,
|
||||
u.create_time
|
||||
FROM sys_user u
|
||||
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
|
||||
WHERE u.del_flag = '0'
|
||||
AND u.account_type = '1'
|
||||
AND (u.nick_name LIKE CONCAT('%', #{name}, '%')
|
||||
OR u.user_name LIKE CONCAT('%', #{name}, '%')
|
||||
OR u.phonenumber LIKE CONCAT('%', #{name}, '%'))
|
||||
AND u.status = '0'
|
||||
ORDER BY u.create_time DESC
|
||||
LIMIT #{limit}
|
||||
""")
|
||||
List<SysTeacherVo> searchTeachersByName(@Param("name") String name, @Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 根据部门ID获取教师列表
|
||||
*
|
||||
* @param deptId 部门ID
|
||||
* @return 教师信息列表
|
||||
*/
|
||||
@Select("""
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.user_name,
|
||||
u.nick_name,
|
||||
u.phonenumber,
|
||||
u.email,
|
||||
u.dept_id,
|
||||
d.dept_name,
|
||||
u.sex,
|
||||
u.status,
|
||||
u.create_time
|
||||
FROM sys_user u
|
||||
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
|
||||
WHERE u.del_flag = '0'
|
||||
AND u.account_type = '1'
|
||||
AND u.dept_id = #{deptId}
|
||||
AND u.status = '0'
|
||||
ORDER BY u.create_time DESC
|
||||
""")
|
||||
List<SysTeacherVo> selectTeachersByDeptId(@Param("deptId") Long deptId);
|
||||
|
||||
/**
|
||||
* 获取部门树形结构(只包含有教师的部门)
|
||||
*
|
||||
* @return 部门树信息集合
|
||||
*/
|
||||
@Select("""
|
||||
SELECT DISTINCT
|
||||
d.dept_id as id,
|
||||
d.parent_id,
|
||||
d.dept_name as label,
|
||||
d.order_num
|
||||
FROM sys_dept d
|
||||
INNER JOIN sys_user u ON d.dept_id = u.dept_id
|
||||
WHERE d.del_flag = '0'
|
||||
AND d.status = '0'
|
||||
AND u.del_flag = '0'
|
||||
AND u.account_type = '1'
|
||||
AND u.status = '0'
|
||||
ORDER BY d.order_num
|
||||
""")
|
||||
List<SysDeptTreeVo> selectDeptTreeWithTeachers();
|
||||
|
||||
/**
|
||||
* 根据部门ID列表获取所有父级部门
|
||||
*
|
||||
* @param deptIds 部门ID列表
|
||||
* @return 父级部门信息集合
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT DISTINCT
|
||||
dept_id as id,
|
||||
parent_id,
|
||||
dept_name as label,
|
||||
order_num
|
||||
FROM sys_dept
|
||||
WHERE del_flag = '0'
|
||||
AND status = '0'
|
||||
AND dept_id IN
|
||||
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
|
||||
#{deptId}
|
||||
</foreach>
|
||||
ORDER BY order_num
|
||||
</script>
|
||||
""")
|
||||
List<SysDeptTreeVo> selectDeptsByIds(@Param("deptIds") List<Long> deptIds);
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package org.dromara.system.service;
|
||||
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.system.domain.bo.SysTeacherBo;
|
||||
import org.dromara.system.domain.vo.SysTeacherVo;
|
||||
import org.dromara.system.domain.vo.SysDeptTreeVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 教师信息管理 服务层
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
public interface ISysTeacherService {
|
||||
|
||||
/**
|
||||
* 分页查询教师信息列表
|
||||
*
|
||||
* @param bo 教师信息
|
||||
* @param pageQuery 分页对象
|
||||
* @return 教师信息集合
|
||||
*/
|
||||
TableDataInfo<SysTeacherVo> queryPageList(SysTeacherBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 根据姓名搜索教师
|
||||
*
|
||||
* @param name 教师姓名关键字
|
||||
* @param limit 查询限制数量
|
||||
* @return 教师信息列表
|
||||
*/
|
||||
List<SysTeacherVo> searchByName(String name, Integer limit);
|
||||
|
||||
/**
|
||||
* 获取部门树形结构
|
||||
*
|
||||
* @return 部门树信息集合
|
||||
*/
|
||||
List<SysDeptTreeVo> getDeptTreeData();
|
||||
|
||||
/**
|
||||
* 根据部门ID获取教师列表
|
||||
*
|
||||
* @param deptId 部门ID
|
||||
* @return 教师信息列表
|
||||
*/
|
||||
List<SysTeacherVo> getTeachersByDeptId(Long deptId);
|
||||
|
||||
/**
|
||||
* 根据ID查询教师详细信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 教师信息
|
||||
*/
|
||||
SysTeacherVo queryById(Long userId);
|
||||
|
||||
/**
|
||||
* 查询教师信息列表
|
||||
*
|
||||
* @param bo 教师信息
|
||||
* @return 教师信息集合
|
||||
*/
|
||||
List<SysTeacherVo> queryList(SysTeacherBo bo);
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
package org.dromara.system.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.domain.bo.SysTeacherBo;
|
||||
import org.dromara.system.domain.vo.SysTeacherVo;
|
||||
import org.dromara.system.domain.vo.SysDeptTreeVo;
|
||||
import org.dromara.system.mapper.SysTeacherMapper;
|
||||
import org.dromara.system.service.ISysTeacherService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 教师信息管理 服务实现
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class SysTeacherServiceImpl implements ISysTeacherService {
|
||||
|
||||
private final SysTeacherMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 分页查询教师信息列表
|
||||
*
|
||||
* @param bo 教师信息
|
||||
* @param pageQuery 分页对象
|
||||
* @return 教师信息集合
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<SysTeacherVo> queryPageList(SysTeacherBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<SysUser> lqw = buildQueryWrapper(bo);
|
||||
Page<SysTeacherVo> page = baseMapper.selectPageTeacherList(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询教师信息列表
|
||||
*
|
||||
* @param bo 教师信息
|
||||
* @return 教师信息集合
|
||||
*/
|
||||
@Override
|
||||
public List<SysTeacherVo> queryList(SysTeacherBo bo) {
|
||||
LambdaQueryWrapper<SysUser> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectTeacherList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据姓名搜索教师
|
||||
*
|
||||
* @param name 教师姓名关键字
|
||||
* @param limit 查询限制数量
|
||||
* @return 教师信息列表
|
||||
*/
|
||||
@Override
|
||||
public List<SysTeacherVo> searchByName(String name, Integer limit) {
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
if (limit == null || limit <= 0) {
|
||||
limit = 10;
|
||||
}
|
||||
return baseMapper.searchTeachersByName(name, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门树形结构
|
||||
*
|
||||
* @return 部门树信息集合
|
||||
*/
|
||||
@Override
|
||||
public List<SysDeptTreeVo> getDeptTreeData() {
|
||||
// 获取有教师的部门
|
||||
List<SysDeptTreeVo> teacherDepts = baseMapper.selectDeptTreeWithTeachers();
|
||||
|
||||
// 补全父级部门
|
||||
List<SysDeptTreeVo> completeDepts = completeParentDepts(teacherDepts);
|
||||
|
||||
// 构建树形结构
|
||||
return buildDeptTree(completeDepts, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全父级部门
|
||||
*
|
||||
* @param teacherDepts 有教师的部门列表
|
||||
* @return 补全父级部门后的完整列表
|
||||
*/
|
||||
private List<SysDeptTreeVo> completeParentDepts(List<SysDeptTreeVo> teacherDepts) {
|
||||
Set<Long> allDeptIds = new HashSet<>();
|
||||
Set<Long> parentIds = new HashSet<>();
|
||||
|
||||
// 收集所有部门ID和父级ID
|
||||
for (SysDeptTreeVo dept : teacherDepts) {
|
||||
allDeptIds.add(dept.getId());
|
||||
if (dept.getParentId() != null && dept.getParentId() != 0L) {
|
||||
collectParentIds(dept.getParentId(), parentIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 找出缺失的父级部门ID
|
||||
Set<Long> missingParentIds = new HashSet<>();
|
||||
for (Long parentId : parentIds) {
|
||||
if (!allDeptIds.contains(parentId)) {
|
||||
missingParentIds.add(parentId);
|
||||
}
|
||||
}
|
||||
|
||||
// 查询缺失的父级部门
|
||||
List<SysDeptTreeVo> completeDepts = new ArrayList<>(teacherDepts);
|
||||
if (!missingParentIds.isEmpty()) {
|
||||
List<SysDeptTreeVo> parentDepts = baseMapper.selectDeptsByIds(new ArrayList<>(missingParentIds));
|
||||
completeDepts.addAll(parentDepts);
|
||||
|
||||
// 递归补全更上级的父级部门
|
||||
List<SysDeptTreeVo> moreParents = completeParentDepts(parentDepts);
|
||||
for (SysDeptTreeVo parent : moreParents) {
|
||||
if (!allDeptIds.contains(parent.getId()) && !missingParentIds.contains(parent.getId())) {
|
||||
completeDepts.add(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return completeDepts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集所有父级部门ID
|
||||
*
|
||||
* @param parentId 父级部门ID
|
||||
* @param parentIds 父级ID集合
|
||||
*/
|
||||
private void collectParentIds(Long parentId, Set<Long> parentIds) {
|
||||
if (parentId != null && parentId != 0L) {
|
||||
parentIds.add(parentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建部门树形结构
|
||||
*
|
||||
* @param flatList 扁平部门列表
|
||||
* @param parentId 父部门ID
|
||||
* @return 树形结构列表
|
||||
*/
|
||||
private List<SysDeptTreeVo> buildDeptTree(List<SysDeptTreeVo> flatList, Long parentId) {
|
||||
List<SysDeptTreeVo> tree = new ArrayList<>();
|
||||
|
||||
for (SysDeptTreeVo dept : flatList) {
|
||||
if (ObjectUtil.equal(dept.getParentId(), parentId)) {
|
||||
List<SysDeptTreeVo> children = buildDeptTree(flatList, dept.getId());
|
||||
dept.setChildren(children);
|
||||
tree.add(dept);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门ID获取教师列表
|
||||
*
|
||||
* @param deptId 部门ID
|
||||
* @return 教师信息列表
|
||||
*/
|
||||
@Override
|
||||
public List<SysTeacherVo> getTeachersByDeptId(Long deptId) {
|
||||
if (ObjectUtil.isNull(deptId)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return baseMapper.selectTeachersByDeptId(deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询教师详细信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 教师信息
|
||||
*/
|
||||
@Override
|
||||
public SysTeacherVo queryById(Long userId) {
|
||||
if (ObjectUtil.isNull(userId)) {
|
||||
return null;
|
||||
}
|
||||
LambdaQueryWrapper<SysUser> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(SysUser::getUserId, userId);
|
||||
lqw.eq(SysUser::getDelFlag, SystemConstants.NORMAL);
|
||||
lqw.eq(SysUser::getAccountType, "1"); // 教师账号类型
|
||||
List<SysTeacherVo> list = baseMapper.selectTeacherList(lqw);
|
||||
return CollUtil.isNotEmpty(list) ? list.get(0) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*
|
||||
* @param bo 教师信息业务对象
|
||||
* @return 查询条件
|
||||
*/
|
||||
private LambdaQueryWrapper<SysUser> buildQueryWrapper(SysTeacherBo bo) {
|
||||
LambdaQueryWrapper<SysUser> lqw = Wrappers.lambdaQuery();
|
||||
|
||||
// 基础条件:未删除的教师用户
|
||||
lqw.eq(SysUser::getDelFlag, SystemConstants.NORMAL);
|
||||
lqw.eq(SysUser::getAccountType, "1"); // 教师账号类型
|
||||
|
||||
// 动态查询条件
|
||||
lqw.eq(ObjectUtil.isNotNull(bo.getUserId()), SysUser::getUserId, bo.getUserId());
|
||||
lqw.like(StrUtil.isNotBlank(bo.getUserName()), SysUser::getUserName, bo.getUserName());
|
||||
lqw.like(StrUtil.isNotBlank(bo.getNickName()), SysUser::getNickName, bo.getNickName());
|
||||
lqw.eq(StrUtil.isNotBlank(bo.getPhonenumber()), SysUser::getPhonenumber, bo.getPhonenumber());
|
||||
lqw.like(StrUtil.isNotBlank(bo.getEmail()), SysUser::getEmail, bo.getEmail());
|
||||
lqw.eq(ObjectUtil.isNotNull(bo.getDeptId()), SysUser::getDeptId, bo.getDeptId());
|
||||
lqw.eq(StrUtil.isNotBlank(bo.getSex()), SysUser::getSex, bo.getSex());
|
||||
lqw.eq(StrUtil.isNotBlank(bo.getStatus()), SysUser::getStatus, bo.getStatus());
|
||||
|
||||
// 搜索关键词(支持姓名、账号、手机号)
|
||||
if (StrUtil.isNotBlank(bo.getSearchKeyword())) {
|
||||
String keyword = bo.getSearchKeyword();
|
||||
lqw.and(wrapper -> wrapper
|
||||
.like(SysUser::getNickName, keyword)
|
||||
.or()
|
||||
.like(SysUser::getUserName, keyword)
|
||||
.or()
|
||||
.like(SysUser::getPhonenumber, keyword)
|
||||
);
|
||||
}
|
||||
|
||||
return lqw;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.dromara.xg.config;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 学工模块缓存配置类
|
||||
* 配置Redis缓存,提升学生数据查询性能
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class XgCacheConfig {
|
||||
|
||||
/**
|
||||
* 配置学工模块Redis缓存管理器
|
||||
*/
|
||||
@Bean("xgCacheManager")
|
||||
public CacheManager xgCacheManager(RedisConnectionFactory factory) {
|
||||
// 默认缓存配置
|
||||
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
|
||||
.entryTtl(Duration.ofMinutes(30)) // 默认30分钟过期
|
||||
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
|
||||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
|
||||
.disableCachingNullValues(); // 不缓存null值
|
||||
|
||||
// 特定缓存配置
|
||||
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
|
||||
|
||||
// 学生树形数据缓存:缓存2小时,因为组织架构变化不频繁
|
||||
cacheConfigurations.put("student_tree", defaultConfig.entryTtl(Duration.ofHours(2)));
|
||||
|
||||
// 学生列表缓存:缓存15分钟,因为学生数据可能变化较频繁
|
||||
cacheConfigurations.put("student_list", defaultConfig.entryTtl(Duration.ofMinutes(15)));
|
||||
|
||||
return RedisCacheManager.builder(factory)
|
||||
.cacheDefaults(defaultConfig)
|
||||
.withInitialCacheConfigurations(cacheConfigurations)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package org.dromara.xg.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.common.core.validate.EditGroup;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
||||
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.xg.domain.bo.XgStudentBo;
|
||||
import org.dromara.xg.domain.vo.XgStudentVo;
|
||||
import org.dromara.xg.domain.vo.XgStudentTreeVo;
|
||||
import org.dromara.xg.service.IXgStudentService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 学生信息管理
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/xg/student")
|
||||
public class XgStudentController extends BaseController {
|
||||
|
||||
private final IXgStudentService xgStudentService;
|
||||
|
||||
/**
|
||||
* 查询学生信息管理列表
|
||||
*/
|
||||
@SaCheckPermission("xg:student:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<XgStudentVo> list(XgStudentBo bo, PageQuery pageQuery) {
|
||||
return xgStudentService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据姓名搜索学生
|
||||
*/
|
||||
@SaCheckPermission("xg:student:list")
|
||||
@GetMapping("/search")
|
||||
public R<List<XgStudentVo>> searchByName(@RequestParam String name,
|
||||
@RequestParam(defaultValue = "10") Integer limit) {
|
||||
List<XgStudentVo> list = xgStudentService.searchByName(name, limit);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学院专业班级树形结构
|
||||
*/
|
||||
@SaCheckPermission("xg:student:list")
|
||||
@GetMapping("/tree")
|
||||
public R<List<XgStudentTreeVo>> getTreeData() {
|
||||
List<XgStudentTreeVo> tree = xgStudentService.getTreeData();
|
||||
return R.ok(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据班级ID获取学生列表
|
||||
*/
|
||||
@SaCheckPermission("xg:student:list")
|
||||
@GetMapping("/class/{classId}")
|
||||
public R<List<XgStudentVo>> getStudentsByClassId(@PathVariable Long classId) {
|
||||
List<XgStudentVo> list = xgStudentService.getStudentsByClassId(classId);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出学生信息管理列表
|
||||
*/
|
||||
@SaCheckPermission("xg:student:export")
|
||||
@Log(title = "学生信息管理", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(XgStudentBo bo, HttpServletResponse response) {
|
||||
List<XgStudentVo> list = xgStudentService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "学生信息管理", XgStudentVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学生信息管理详细信息
|
||||
*/
|
||||
@SaCheckPermission("xg:student:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<XgStudentVo> getInfo(@NotNull(message = "主键不能为空")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(xgStudentService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增学生信息管理
|
||||
*/
|
||||
@SaCheckPermission("xg:student:add")
|
||||
@Log(title = "学生信息管理", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgStudentBo bo) {
|
||||
return toAjax(xgStudentService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改学生信息管理
|
||||
*/
|
||||
@SaCheckPermission("xg:student:edit")
|
||||
@Log(title = "学生信息管理", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgStudentBo bo) {
|
||||
return toAjax(xgStudentService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除学生信息管理
|
||||
*/
|
||||
@SaCheckPermission("xg:student:remove")
|
||||
@Log(title = "学生信息管理", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空")
|
||||
@PathVariable Long[] ids) {
|
||||
return toAjax(xgStudentService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package org.dromara.xg.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.common.core.validate.EditGroup;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
import org.dromara.xg.domain.XgStudentMsg;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 学生信息业务对象 xg_student_msg
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = XgStudentMsg.class, reverseConvertGenerate = false)
|
||||
public class XgStudentBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 主键ID(雪花ID)
|
||||
*/
|
||||
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
@NotBlank(message = "学号不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String studentNo;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
@NotBlank(message = "姓名不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 性别:0-男,1-女
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 学院ID
|
||||
*/
|
||||
private String collegeId;
|
||||
|
||||
/**
|
||||
* 学院名称
|
||||
*/
|
||||
private String collegeName;
|
||||
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
private String majorId;
|
||||
|
||||
/**
|
||||
* 专业名称
|
||||
*/
|
||||
private String majorName;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
private String classId;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
private String className;
|
||||
|
||||
/**
|
||||
* 年级
|
||||
*/
|
||||
private String grade;
|
||||
|
||||
/**
|
||||
* 学籍状态:1-在籍,2-休学,3-复学,4-退学,5-毕业
|
||||
*/
|
||||
private String studentStatus;
|
||||
|
||||
/**
|
||||
* 入学年份
|
||||
*/
|
||||
private Integer enrollmentYear;
|
||||
|
||||
/**
|
||||
* 入学日期
|
||||
*/
|
||||
private Date enrollmentDate;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package org.dromara.xg.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 学生树形结构视图对象
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Data
|
||||
public class XgStudentTreeVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 节点ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 节点名称
|
||||
*/
|
||||
private String label;
|
||||
|
||||
/**
|
||||
* 节点类型: college-学院, major-专业, class-班级
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 子节点
|
||||
*/
|
||||
private List<XgStudentTreeVo> children;
|
||||
|
||||
/**
|
||||
* 学生数量(仅班级节点有)
|
||||
*/
|
||||
private Integer studentCount;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package org.dromara.xg.domain.vo;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.xg.domain.XgStudentMsg;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 学生信息视图对象 xg_student_msg
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = XgStudentMsg.class)
|
||||
public class XgStudentVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID(雪花ID)
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
private String studentNo;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 性别:0-男,1-女
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 学院ID
|
||||
*/
|
||||
private String collegeId;
|
||||
|
||||
/**
|
||||
* 学院名称
|
||||
*/
|
||||
private String collegeName;
|
||||
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
private String majorId;
|
||||
|
||||
/**
|
||||
* 专业名称
|
||||
*/
|
||||
private String majorName;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
private String classId;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
private String className;
|
||||
|
||||
/**
|
||||
* 年级
|
||||
*/
|
||||
private String grade;
|
||||
|
||||
/**
|
||||
* 学籍状态:1-在籍,2-休学,3-复学,4-退学,5-毕业
|
||||
*/
|
||||
private String studentStatus;
|
||||
|
||||
/**
|
||||
* 入学年份
|
||||
*/
|
||||
private Integer enrollmentYear;
|
||||
|
||||
/**
|
||||
* 入学日期
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date enrollmentDate;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package org.dromara.xg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.xg.domain.XgStudentMsg;
|
||||
import org.dromara.xg.domain.vo.XgStudentVo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 学生信息管理Mapper接口
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
public interface XgStudentMapper extends BaseMapperPlus<XgStudentMsg, XgStudentVo> {
|
||||
|
||||
/**
|
||||
* 查询树形结构所需的最少数据
|
||||
* 只查询构建树形结构必需的字段,避免全表查询,提升性能
|
||||
*/
|
||||
@Select("SELECT DISTINCT " +
|
||||
"college_id, college_name, " +
|
||||
"major_id, major_name, " +
|
||||
"class_id, class_name " +
|
||||
"FROM xg_student_msg " +
|
||||
"WHERE deleted = 0 " +
|
||||
"AND student_status = '1' " +
|
||||
"AND college_id IS NOT NULL " +
|
||||
"AND major_id IS NOT NULL " +
|
||||
"AND class_id IS NOT NULL " +
|
||||
"ORDER BY college_name, major_name, class_name")
|
||||
List<Map<String, Object>> selectTreeNodes();
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package org.dromara.xg.service;
|
||||
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.xg.domain.bo.XgStudentBo;
|
||||
import org.dromara.xg.domain.vo.XgStudentVo;
|
||||
import org.dromara.xg.domain.vo.XgStudentTreeVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 学生信息管理Service接口
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
public interface IXgStudentService {
|
||||
|
||||
/**
|
||||
* 查询学生信息管理
|
||||
*/
|
||||
XgStudentVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询学生信息管理列表
|
||||
*/
|
||||
TableDataInfo<XgStudentVo> queryPageList(XgStudentBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询学生信息管理列表
|
||||
*/
|
||||
List<XgStudentVo> queryList(XgStudentBo bo);
|
||||
|
||||
/**
|
||||
* 根据姓名搜索学生
|
||||
*/
|
||||
List<XgStudentVo> searchByName(String name, Integer limit);
|
||||
|
||||
/**
|
||||
* 获取学院专业班级树形结构
|
||||
*/
|
||||
List<XgStudentTreeVo> getTreeData();
|
||||
|
||||
/**
|
||||
* 根据班级ID获取学生列表
|
||||
*/
|
||||
List<XgStudentVo> getStudentsByClassId(Long classId);
|
||||
|
||||
/**
|
||||
* 新增学生信息管理
|
||||
*/
|
||||
Boolean insertByBo(XgStudentBo bo);
|
||||
|
||||
/**
|
||||
* 修改学生信息管理
|
||||
*/
|
||||
Boolean updateByBo(XgStudentBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除学生信息管理信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package org.dromara.xg.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 学生数据缓存清除服务
|
||||
* 用于在学生数据更新时清除相关缓存,保证数据一致性
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class XgStudentCacheService {
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
/**
|
||||
* 清除学生树形数据缓存
|
||||
* 当学院、专业、班级信息发生变化时调用
|
||||
*/
|
||||
public void evictTreeCache() {
|
||||
try {
|
||||
Objects.requireNonNull(cacheManager.getCache("student_tree")).clear();
|
||||
log.info("清除学生树形数据缓存成功");
|
||||
} catch (Exception e) {
|
||||
log.warn("清除学生树形数据缓存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定班级的学生列表缓存
|
||||
* 当该班级的学生信息发生变化时调用
|
||||
*/
|
||||
public void evictStudentListCache(Long classId) {
|
||||
try {
|
||||
String cacheKey = "class_" + classId;
|
||||
Objects.requireNonNull(cacheManager.getCache("student_list")).evict(cacheKey);
|
||||
log.info("清除班级[{}]学生列表缓存成功", classId);
|
||||
} catch (Exception e) {
|
||||
log.warn("清除班级[{}]学生列表缓存失败", classId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有学生列表缓存
|
||||
* 当有大批量学生数据更新时调用
|
||||
*/
|
||||
public void evictAllStudentListCache() {
|
||||
try {
|
||||
Objects.requireNonNull(cacheManager.getCache("student_list")).clear();
|
||||
log.info("清除所有学生列表缓存成功");
|
||||
} catch (Exception e) {
|
||||
log.warn("清除所有学生列表缓存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有相关缓存
|
||||
* 当进行大规模数据更新或系统维护时调用
|
||||
*/
|
||||
public void evictAllCache() {
|
||||
evictTreeCache();
|
||||
evictAllStudentListCache();
|
||||
log.info("清除所有学生相关缓存完成");
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
package org.dromara.xg.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.xg.domain.XgStudentMsg;
|
||||
import org.dromara.xg.domain.bo.XgStudentBo;
|
||||
import org.dromara.xg.domain.vo.XgStudentTreeVo;
|
||||
import org.dromara.xg.domain.vo.XgStudentVo;
|
||||
import org.dromara.xg.mapper.XgStudentMapper;
|
||||
import org.dromara.xg.service.IXgStudentService;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 学生信息管理Service业务层处理
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class XgStudentServiceImpl implements IXgStudentService {
|
||||
|
||||
private final XgStudentMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询学生信息管理
|
||||
*/
|
||||
@Override
|
||||
public XgStudentVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询学生信息管理列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<XgStudentVo> queryPageList(XgStudentBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<XgStudentMsg> lqw = buildQueryWrapper(bo);
|
||||
Page<XgStudentVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询学生信息管理列表
|
||||
*/
|
||||
@Override
|
||||
public List<XgStudentVo> queryList(XgStudentBo bo) {
|
||||
LambdaQueryWrapper<XgStudentMsg> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据姓名搜索学生
|
||||
*/
|
||||
@Override
|
||||
public List<XgStudentVo> searchByName(String name, Integer limit) {
|
||||
LambdaQueryWrapper<XgStudentMsg> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(name), XgStudentMsg::getName, name);
|
||||
lqw.eq(XgStudentMsg::getStudentStatus, "1"); // 只查询在籍学生
|
||||
lqw.last("LIMIT " + (limit != null ? limit : 10));
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学院专业班级树形结构
|
||||
* 优化版本:使用专门的SQL查询,避免全表查询,提升性能
|
||||
*/
|
||||
@Override
|
||||
@Cacheable(value = "student_tree", key = "'tree_data'", unless = "#result == null || #result.isEmpty()")
|
||||
public List<XgStudentTreeVo> getTreeData() {
|
||||
// 使用专门的SQL查询获取树形结构所需的最少数据
|
||||
List<Map<String, Object>> treeNodes = baseMapper.selectTreeNodes();
|
||||
|
||||
// 构建树形结构
|
||||
Map<String, XgStudentTreeVo> collegeMap = new LinkedHashMap<>();
|
||||
Map<String, XgStudentTreeVo> majorMap = new LinkedHashMap<>();
|
||||
Map<String, XgStudentTreeVo> classMap = new LinkedHashMap<>();
|
||||
|
||||
for (Map<String, Object> node : treeNodes) {
|
||||
String collegeId = (String) node.get("college_id");
|
||||
String collegeName = (String) node.get("college_name");
|
||||
String majorId = (String) node.get("major_id");
|
||||
String majorName = (String) node.get("major_name");
|
||||
String classId = (String) node.get("class_id");
|
||||
String className = (String) node.get("class_name");
|
||||
|
||||
// 学院节点
|
||||
if (StringUtils.isNotBlank(collegeId) && !collegeMap.containsKey(collegeId)) {
|
||||
XgStudentTreeVo college = new XgStudentTreeVo();
|
||||
college.setId(collegeId);
|
||||
college.setLabel(collegeName);
|
||||
college.setType("college");
|
||||
college.setChildren(new ArrayList<>());
|
||||
collegeMap.put(collegeId, college);
|
||||
}
|
||||
|
||||
// 专业节点
|
||||
String majorKey = collegeId + "_" + majorId;
|
||||
if (StringUtils.isNotBlank(majorId) && !majorMap.containsKey(majorKey)) {
|
||||
XgStudentTreeVo major = new XgStudentTreeVo();
|
||||
major.setId(majorId);
|
||||
major.setLabel(majorName);
|
||||
major.setType("major");
|
||||
major.setChildren(new ArrayList<>());
|
||||
majorMap.put(majorKey, major);
|
||||
|
||||
// 添加到学院下
|
||||
if (collegeMap.containsKey(collegeId)) {
|
||||
collegeMap.get(collegeId).getChildren().add(major);
|
||||
}
|
||||
}
|
||||
|
||||
// 班级节点
|
||||
String classKey = majorKey + "_" + classId;
|
||||
if (StringUtils.isNotBlank(classId) && !classMap.containsKey(classKey)) {
|
||||
XgStudentTreeVo clazz = new XgStudentTreeVo();
|
||||
clazz.setId(classId);
|
||||
clazz.setLabel(className);
|
||||
clazz.setType("class");
|
||||
clazz.setChildren(new ArrayList<>());
|
||||
classMap.put(classKey, clazz);
|
||||
|
||||
// 添加到专业下
|
||||
if (majorMap.containsKey(majorKey)) {
|
||||
majorMap.get(majorKey).getChildren().add(clazz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(collegeMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据班级ID获取学生列表
|
||||
*/
|
||||
@Override
|
||||
@Cacheable(value = "student_list", key = "'class_' + #classId", unless = "#result == null || #result.isEmpty()")
|
||||
public List<XgStudentVo> getStudentsByClassId(Long classId) {
|
||||
LambdaQueryWrapper<XgStudentMsg> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(XgStudentMsg::getClassId, classId);
|
||||
lqw.eq(XgStudentMsg::getStudentStatus, "1"); // 只查询在籍学生
|
||||
lqw.orderByAsc(XgStudentMsg::getStudentNo); // 按学号排序
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增学生信息管理
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(XgStudentBo bo) {
|
||||
XgStudentMsg add = MapstructUtils.convert(bo, XgStudentMsg.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改学生信息管理
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(XgStudentBo bo) {
|
||||
XgStudentMsg update = MapstructUtils.convert(bo, XgStudentMsg.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(XgStudentMsg entity) {
|
||||
// TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除学生信息管理
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if (isValid) {
|
||||
// TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<XgStudentMsg> buildQueryWrapper(XgStudentBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<XgStudentMsg> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getStudentNo()), XgStudentMsg::getStudentNo, bo.getStudentNo());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getName()), XgStudentMsg::getName, bo.getName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getGender()), XgStudentMsg::getGender, bo.getGender());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getPhone()), XgStudentMsg::getPhone, bo.getPhone());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getCollegeId()), XgStudentMsg::getCollegeId, bo.getCollegeId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getMajorId()), XgStudentMsg::getMajorId, bo.getMajorId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getClassId()), XgStudentMsg::getClassId, bo.getClassId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getGrade()), XgStudentMsg::getGrade, bo.getGrade());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getStudentStatus()), XgStudentMsg::getStudentStatus, bo.getStudentStatus());
|
||||
lqw.eq(bo.getEnrollmentYear() != null, XgStudentMsg::getEnrollmentYear, bo.getEnrollmentYear());
|
||||
lqw.eq(bo.getEnrollmentDate() != null, XgStudentMsg::getEnrollmentDate, bo.getEnrollmentDate());
|
||||
return lqw;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { TeacherVO, TeacherQuery, DeptTreeNode } from './types';
|
||||
import { TableDataInfo } from '@/api/types';
|
||||
|
||||
/**
|
||||
* 查询教师信息列表
|
||||
*/
|
||||
export function listTeacher(query: TeacherQuery): AxiosPromise<TableDataInfo<TeacherVO>> {
|
||||
return request({
|
||||
url: '/system/teacher/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取教师详细信息
|
||||
*/
|
||||
export function getTeacher(userId: string | number): AxiosPromise<TeacherVO> {
|
||||
return request({
|
||||
url: '/system/teacher/' + userId,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据姓名搜索教师
|
||||
*/
|
||||
export function searchTeacherByName(name: string, limit = 10): AxiosPromise<TeacherVO[]> {
|
||||
return request({
|
||||
url: '/system/teacher/search',
|
||||
method: 'get',
|
||||
params: { name, limit }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门树形结构
|
||||
*/
|
||||
export function getDeptTreeData(): AxiosPromise<DeptTreeNode[]> {
|
||||
return request({
|
||||
url: '/system/teacher/deptTree',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门ID获取教师列表
|
||||
*/
|
||||
export function getTeachersByDeptId(deptId: string | number): AxiosPromise<TeacherVO[]> {
|
||||
return request({
|
||||
url: '/system/teacher/dept/' + deptId,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { PageQuery } from '@/api/types';
|
||||
|
||||
/**
|
||||
* 教师信息视图对象
|
||||
*/
|
||||
export interface TeacherVO {
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
userId: string | number;
|
||||
|
||||
/**
|
||||
* 用户账号
|
||||
*/
|
||||
userName: string;
|
||||
|
||||
/**
|
||||
* 用户昵称(教师姓名)
|
||||
*/
|
||||
nickName: string;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
phonenumber: string;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
deptId: string | number;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
deptName: string;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*/
|
||||
sex: string;
|
||||
|
||||
/**
|
||||
* 帐号状态
|
||||
*/
|
||||
status: string;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 教师查询对象
|
||||
*/
|
||||
export interface TeacherQuery extends PageQuery {
|
||||
/**
|
||||
* 用户昵称(教师姓名)
|
||||
*/
|
||||
nickName?: string;
|
||||
|
||||
/**
|
||||
* 用户账号
|
||||
*/
|
||||
userName?: string;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
phonenumber?: string;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
deptId?: string | number;
|
||||
|
||||
/**
|
||||
* 帐号状态
|
||||
*/
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门树形节点
|
||||
*/
|
||||
export interface DeptTreeNode {
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* 子部门
|
||||
*/
|
||||
children?: DeptTreeNode[];
|
||||
|
||||
/**
|
||||
* 教师数量
|
||||
*/
|
||||
teacherCount?: number;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { StudentVO, StudentQuery, StudentTreeNode } from './types';
|
||||
import { TableDataInfo } from '@/api/types';
|
||||
|
||||
/**
|
||||
* 查询学生信息列表
|
||||
*/
|
||||
export function listStudent(query: StudentQuery): AxiosPromise<TableDataInfo<StudentVO>> {
|
||||
return request({
|
||||
url: '/xg/student/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学生详细信息
|
||||
*/
|
||||
export function getStudent(id: string | number): AxiosPromise<StudentVO> {
|
||||
return request({
|
||||
url: '/xg/student/' + id,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据姓名搜索学生
|
||||
*/
|
||||
export function searchStudentByName(name: string, limit = 10): AxiosPromise<StudentVO[]> {
|
||||
return request({
|
||||
url: '/xg/student/search',
|
||||
method: 'get',
|
||||
params: { name, limit }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学院专业班级树形结构
|
||||
*/
|
||||
export function getStudentTreeData(): AxiosPromise<StudentTreeNode[]> {
|
||||
return request({
|
||||
url: '/xg/student/tree',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据班级ID获取学生列表
|
||||
*/
|
||||
export function getStudentsByClassId(classId: string | number): AxiosPromise<StudentVO[]> {
|
||||
return request({
|
||||
url: '/xg/student/class/' + classId,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { PageQuery } from '@/api/types';
|
||||
|
||||
/**
|
||||
* 学生信息视图对象
|
||||
*/
|
||||
export interface StudentVO {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo: string;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 性别
|
||||
*/
|
||||
gender: string;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
phone: string;
|
||||
|
||||
/**
|
||||
* 院系ID
|
||||
*/
|
||||
collegeId: string;
|
||||
|
||||
/**
|
||||
* 院系名称
|
||||
*/
|
||||
collegeName: string;
|
||||
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
majorId: string;
|
||||
|
||||
/**
|
||||
* 专业名称
|
||||
*/
|
||||
majorName: string;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
classId: string;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* 年级
|
||||
*/
|
||||
grade: string;
|
||||
|
||||
/**
|
||||
* 学籍状态
|
||||
*/
|
||||
studentStatus: string;
|
||||
|
||||
/**
|
||||
* 入学年份
|
||||
*/
|
||||
enrollmentYear: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 学生查询对象
|
||||
*/
|
||||
export interface StudentQuery extends PageQuery {
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo?: string;
|
||||
|
||||
/**
|
||||
* 院系ID
|
||||
*/
|
||||
collegeId?: string;
|
||||
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
majorId?: string;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
classId?: string;
|
||||
|
||||
/**
|
||||
* 年级
|
||||
*/
|
||||
grade?: string;
|
||||
|
||||
/**
|
||||
* 学籍状态
|
||||
*/
|
||||
studentStatus?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 学院专业班级树形节点
|
||||
*/
|
||||
export interface StudentTreeNode {
|
||||
/**
|
||||
* 节点ID
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 节点名称
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* 节点类型: college-学院, major-专业, class-班级
|
||||
*/
|
||||
type: 'college' | 'major' | 'class';
|
||||
|
||||
/**
|
||||
* 子节点
|
||||
*/
|
||||
children?: StudentTreeNode[];
|
||||
|
||||
/**
|
||||
* 学生数量(仅班级节点有)
|
||||
*/
|
||||
studentCount?: number;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="activity-selector">
|
||||
<el-input
|
||||
v-model="selectedActivityName"
|
||||
placeholder="请选择活动"
|
||||
readonly
|
||||
@click="openSelector"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="el-input__icon"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 活动选择对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="选择活动"
|
||||
width="800px"
|
||||
append-to-body
|
||||
>
|
||||
<!-- 搜索框 -->
|
||||
<div class="mb-4">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入活动名称搜索"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
style="width: 300px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 活动列表 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="activityList"
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
style="width: 100%"
|
||||
max-height="400px"
|
||||
>
|
||||
<el-table-column label="活动名称" prop="activityName" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="活动类型" prop="activityType" width="120" />
|
||||
<el-table-column label="活动状态" prop="activityStatus" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.activityStatus) as any">
|
||||
{{ getStatusText(scope.row.activityStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" prop="startTime" width="160">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.startTime">{{ formatDate(scope.row.startTime) }}</span>
|
||||
<span v-else class="text-gray-400">未设置</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container mt-4">
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="getActivityList"
|
||||
@current-change="getActivityList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmSelect" :disabled="!selectedActivity">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { listStglActivity } from '@/api/stgl/activity';
|
||||
|
||||
interface ActivityOption {
|
||||
id: string | number;
|
||||
activityName?: string;
|
||||
activityType?: string;
|
||||
activityStatus?: string;
|
||||
startTime?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string | number): void;
|
||||
(e: 'change', value: string | number): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const activityList = ref<ActivityOption[]>([]);
|
||||
const total = ref(0);
|
||||
const selectedActivity = ref<ActivityOption | null>(null);
|
||||
const selectedActivityName = ref('');
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
activityName: ''
|
||||
});
|
||||
|
||||
/** 打开选择器 */
|
||||
const openSelector = () => {
|
||||
dialogVisible.value = true;
|
||||
getActivityList();
|
||||
};
|
||||
|
||||
/** 获取活动列表 */
|
||||
const getActivityList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await listStglActivity(queryParams);
|
||||
activityList.value = res.rows;
|
||||
total.value = res.total;
|
||||
} catch (error) {
|
||||
console.error('获取活动列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 搜索处理 */
|
||||
const handleSearch = () => {
|
||||
queryParams.activityName = searchKeyword.value;
|
||||
queryParams.pageNum = 1;
|
||||
getActivityList();
|
||||
};
|
||||
|
||||
/** 表格当前行变化 */
|
||||
const handleCurrentChange = (val: ActivityOption) => {
|
||||
selectedActivity.value = val;
|
||||
};
|
||||
|
||||
/** 确认选择 */
|
||||
const confirmSelect = () => {
|
||||
if (selectedActivity.value) {
|
||||
const activityId = selectedActivity.value.id;
|
||||
selectedActivityName.value = selectedActivity.value.activityName;
|
||||
|
||||
emit('update:modelValue', activityId);
|
||||
emit('change', activityId);
|
||||
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取状态类型 */
|
||||
const getStatusType = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'1': 'info', // 筹备中
|
||||
'2': 'warning', // 待审批
|
||||
'3': 'success', // 已通过
|
||||
'4': 'primary', // 进行中
|
||||
'5': 'info', // 已结束
|
||||
'6': 'danger', // 已取消
|
||||
'7': 'danger' // 已驳回
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
};
|
||||
|
||||
/** 获取状态文本 */
|
||||
const getStatusText = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'1': '筹备中',
|
||||
'2': '待审批',
|
||||
'3': '已通过',
|
||||
'4': '进行中',
|
||||
'5': '已结束',
|
||||
'6': '已取消',
|
||||
'7': '已驳回'
|
||||
};
|
||||
return statusMap[status] || '未知';
|
||||
};
|
||||
|
||||
/** 格式化日期 */
|
||||
const formatDate = (dateStr: string): string => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN');
|
||||
};
|
||||
|
||||
/** 监听外部值变化 */
|
||||
watch(() => props.modelValue, async (newVal) => {
|
||||
if (newVal) {
|
||||
try {
|
||||
// 这里可以根据ID获取活动名称
|
||||
const res = await listStglActivity({
|
||||
pageNum: 1,
|
||||
pageSize: 1
|
||||
});
|
||||
if (res.rows && res.rows.length > 0) {
|
||||
selectedActivityName.value = res.rows[0].activityName;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取活动信息失败:', error);
|
||||
}
|
||||
} else {
|
||||
selectedActivityName.value = '';
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div class="advisor-manager">
|
||||
<div class="manager-header">
|
||||
<span class="title">指导老师管理</span>
|
||||
<el-button type="primary" size="small" @click="addAdvisor">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加指导老师
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="advisors.length === 0" class="empty-tip">暂无指导老师,点击上方按钮添加</div>
|
||||
|
||||
<div v-else class="advisor-list">
|
||||
<div v-for="(advisor, index) in advisors" :key="`advisor-${index}-${advisor.teacher?.userId || 'new'}`" class="advisor-item">
|
||||
<el-row :gutter="2" align="middle">
|
||||
<el-col :span="5">
|
||||
<el-form-item label="姓名" :prop="`advisors.${index}.name`">
|
||||
<TeacherSelector v-model="advisor.teacher" placeholder="选择指导老师" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="联系电话" :prop="`advisors.${index}.phone`">
|
||||
<el-input v-model="advisor.phone" placeholder="联系电话" readonly />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6" style="margin-left: -20px">
|
||||
<el-form-item label="所在部门" :prop="`advisors.${index}.department`">
|
||||
<el-input v-model="advisor.dept" placeholder="所在部门" readonly />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6" style="margin-left: -25px">
|
||||
<el-form-item label="邮箱" :prop="`advisors.${index}.email`">
|
||||
<el-input v-model="advisor.email" placeholder="邮箱" readonly />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col
|
||||
:span="1"
|
||||
style="text-align: center; display: flex; align-items: flex-start; justify-content: center; margin-top: -14px; margin-left: 10px"
|
||||
>
|
||||
<el-button type="danger" size="small" circle @click="removeAdvisor(index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, computed } from 'vue';
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
import TeacherSelector from '@/components/TeacherSelector/index.vue';
|
||||
import { TeacherVO } from '@/api/system/teacher/types';
|
||||
|
||||
export interface AdvisorVO {
|
||||
teacher: TeacherVO | null;
|
||||
name: string;
|
||||
phone: string;
|
||||
dept: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: AdvisorVO[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: AdvisorVO[]): void;
|
||||
(e: 'change', value: AdvisorVO[]): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const advisors = ref<AdvisorVO[]>([]);
|
||||
const updateLock = ref(false);
|
||||
|
||||
// 计算属性:生成当前数据的hash值,用于检测变化
|
||||
const currentHash = computed(() => {
|
||||
return JSON.stringify(
|
||||
advisors.value.map((advisor) => ({
|
||||
userId: advisor.teacher?.userId,
|
||||
name: advisor.name,
|
||||
phone: advisor.phone,
|
||||
dept: advisor.dept,
|
||||
email: advisor.email
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (!updateLock.value) {
|
||||
updateLock.value = true;
|
||||
nextTick(() => {
|
||||
advisors.value = newValue.map((item) => ({ ...item }));
|
||||
updateLock.value = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听内部值变化 - 使用hash比较避免深度比较
|
||||
watch(currentHash, (newHash) => {
|
||||
if (!updateLock.value) {
|
||||
updateLock.value = true;
|
||||
// 延迟emit,避免在TabPane环境下的立即更新冲突
|
||||
nextTick(() => {
|
||||
const newValue = advisors.value.map((advisor) => ({ ...advisor }));
|
||||
emit('update:modelValue', newValue);
|
||||
// 重新添加change事件,但添加防护机制
|
||||
emit('change', newValue);
|
||||
updateLock.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 添加指导老师
|
||||
const addAdvisor = () => {
|
||||
// 使用函数式更新,确保响应性
|
||||
advisors.value = [
|
||||
...advisors.value,
|
||||
{
|
||||
teacher: null,
|
||||
name: '',
|
||||
phone: '',
|
||||
dept: '',
|
||||
email: ''
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// 删除指导老师
|
||||
const removeAdvisor = (index: number) => {
|
||||
// 使用函数式更新,确保响应性
|
||||
advisors.value = advisors.value.filter((_, i) => i !== index);
|
||||
};
|
||||
|
||||
// 监听teacher选择变化,自动填充相关信息
|
||||
watch(
|
||||
() => advisors.value.map((advisor) => advisor.teacher),
|
||||
(newTeachers, oldTeachers) => {
|
||||
console.log('AdvisorManager - teacher选择变化监听器触发');
|
||||
console.log('newTeachers:', newTeachers);
|
||||
console.log('oldTeachers:', oldTeachers);
|
||||
|
||||
newTeachers.forEach((teacher, index) => {
|
||||
const oldTeacher = oldTeachers?.[index];
|
||||
|
||||
console.log(`检查第${index}个指导老师:`, {
|
||||
newTeacher: teacher,
|
||||
oldTeacher: oldTeacher,
|
||||
hasChanged: teacher && (!oldTeacher || teacher.userId !== oldTeacher?.userId)
|
||||
});
|
||||
|
||||
// 检查是否是新选择的老师或者老师发生了变化
|
||||
if (teacher && (!oldTeacher || teacher.userId !== oldTeacher.userId)) {
|
||||
console.log('选择了新的指导老师:', teacher);
|
||||
const advisor = advisors.value[index];
|
||||
if (advisor) {
|
||||
// 自动填充老师信息
|
||||
advisor.name = teacher.nickName || '';
|
||||
advisor.phone = teacher.phonenumber || '';
|
||||
advisor.dept = teacher.deptName || '';
|
||||
advisor.email = teacher.email || '';
|
||||
|
||||
console.log('自动填充的信息:', {
|
||||
name: advisor.name,
|
||||
phone: advisor.phone,
|
||||
dept: advisor.dept,
|
||||
email: advisor.email
|
||||
});
|
||||
}
|
||||
} else if (!teacher && oldTeacher) {
|
||||
// 清空选择时重置所有信息
|
||||
console.log('清空指导老师选择');
|
||||
const advisor = advisors.value[index];
|
||||
if (advisor) {
|
||||
advisor.name = '';
|
||||
advisor.phone = '';
|
||||
advisor.dept = '';
|
||||
advisor.email = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ deep: true, immediate: false }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.advisor-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.manager-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.advisor-list {
|
||||
space-y: 16px;
|
||||
}
|
||||
|
||||
.advisor-item {
|
||||
padding: 16px 8px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 让姓名字段往左移,保持宽度一致 */
|
||||
.advisor-item :deep(.el-col:first-child .el-form-item) {
|
||||
margin-left: -65px !important;
|
||||
}
|
||||
|
||||
.advisor-item :deep(.el-col:nth-child(2) .el-form-item) {
|
||||
margin-left: -20px !important;
|
||||
}
|
||||
|
||||
/* 针对联系电话字段 */
|
||||
.advisor-item :deep(.el-col:nth-child(2) .el-input) {
|
||||
width: 120% !important;
|
||||
}
|
||||
|
||||
/* 针对所在部门字段 */
|
||||
.advisor-item :deep(.el-col:nth-child(3) .el-input) {
|
||||
width: 120% !important;
|
||||
}
|
||||
|
||||
.advisor-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.advisor-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="club-selector">
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
@update:model-value="handleChange"
|
||||
:placeholder="placeholder"
|
||||
:clearable="clearable"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="remoteMethod"
|
||||
:loading="loading"
|
||||
value-key="uniqueKey"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in options"
|
||||
:key="`${option.relatedType}-${option.relatedClubId}`"
|
||||
:label="formatOptionLabel(option)"
|
||||
:value="option"
|
||||
>
|
||||
<div class="option-content">
|
||||
<span class="option-name">{{ option.relatedClubName }}</span>
|
||||
<span class="option-type">{{ getTypeLabel(option.relatedType) }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- 详细选择对话框 -->
|
||||
<el-dialog v-model="dialogVisible" title="选择社团/志愿者队伍" width="800px" append-to-body>
|
||||
<el-tabs v-model="activeTab" type="card">
|
||||
<el-tab-pane label="社团" name="club">
|
||||
<el-table
|
||||
:data="clubOptions"
|
||||
@row-click="handleRowClick"
|
||||
highlight-current-row
|
||||
height="400"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="社团名称" prop="relatedClubName" />
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default>
|
||||
<el-tag type="primary" size="small">社团</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="志愿者队伍" name="volunteer">
|
||||
<el-table
|
||||
:data="volunteerOptions"
|
||||
@row-click="handleRowClick"
|
||||
highlight-current-row
|
||||
height="400"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="队伍名称" prop="relatedClubName" />
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default>
|
||||
<el-tag type="success" size="small">志愿者队伍</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmSelection">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { getClubOptions, getVolunteerTeamOptions } from '@/api/stgl/activity';
|
||||
import { ClubOption } from '@/api/stgl/activity/types';
|
||||
|
||||
interface Props {
|
||||
modelValue?: ClubOption | null;
|
||||
placeholder?: string;
|
||||
clearable?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 'large' | 'default' | 'small';
|
||||
showDialog?: boolean; // 是否显示详细选择对话框
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: ClubOption | null): void;
|
||||
(e: 'change', value: ClubOption | null): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: '请选择社团/志愿者队伍',
|
||||
clearable: true,
|
||||
disabled: false,
|
||||
size: 'default',
|
||||
showDialog: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const activeTab = ref('club');
|
||||
const options = ref<ClubOption[]>([]);
|
||||
const clubOptions = ref<ClubOption[]>([]);
|
||||
const volunteerOptions = ref<ClubOption[]>([]);
|
||||
const selectedOption = ref<ClubOption | null>(null);
|
||||
|
||||
// 格式化选项标签
|
||||
const formatOptionLabel = (option: ClubOption): string => {
|
||||
const typeText = option.relatedType === 1 ? '社团' : '志愿者队伍';
|
||||
return `${option.relatedClubName} (${typeText})`;
|
||||
};
|
||||
|
||||
// 获取类型标签
|
||||
const getTypeLabel = (type: number): string => {
|
||||
return type === 1 ? '社团' : '志愿者队伍';
|
||||
};
|
||||
|
||||
// 处理值变化
|
||||
const handleChange = (value: ClubOption | null) => {
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
};
|
||||
|
||||
// 远程搜索方法
|
||||
const remoteMethod = async (query: string) => {
|
||||
if (query) {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 根据查询条件过滤选项
|
||||
const filteredOptions = [...clubOptions.value, ...volunteerOptions.value].filter(option =>
|
||||
option.relatedClubName.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
options.value = filteredOptions.map(item => ({
|
||||
...item,
|
||||
uniqueKey: item.uniqueKey || `${item.relatedType}-${item.relatedClubId}`
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
options.value = [...clubOptions.value, ...volunteerOptions.value];
|
||||
}
|
||||
};
|
||||
|
||||
// 处理行点击
|
||||
const handleRowClick = (row: ClubOption) => {
|
||||
selectedOption.value = row;
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const confirmSelection = () => {
|
||||
if (selectedOption.value) {
|
||||
handleChange(selectedOption.value);
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载选项数据
|
||||
const loadOptions = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [clubRes, volunteerRes] = await Promise.all([
|
||||
getClubOptions(),
|
||||
getVolunteerTeamOptions()
|
||||
]);
|
||||
|
||||
// 为每个选项添加唯一标识
|
||||
clubOptions.value = (clubRes.data || []).map(item => ({
|
||||
...item,
|
||||
uniqueKey: `${item.relatedType}-${item.relatedClubId}`
|
||||
}));
|
||||
volunteerOptions.value = (volunteerRes.data || []).map(item => ({
|
||||
...item,
|
||||
uniqueKey: `${item.relatedType}-${item.relatedClubId}`
|
||||
}));
|
||||
options.value = [...clubOptions.value, ...volunteerOptions.value];
|
||||
} catch (error) {
|
||||
console.error('加载选项失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.club-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.option-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.option-type {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div class="co-organizer-manager">
|
||||
<div class="header">
|
||||
<span class="title">承办方信息</span>
|
||||
<el-button type="primary" size="small" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加承办方
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="coOrganizers.length === 0" class="empty-state">
|
||||
<el-empty description="暂无承办方信息" :image-size="80" />
|
||||
</div>
|
||||
|
||||
<div v-else class="organizer-list">
|
||||
<div
|
||||
v-for="(organizer, index) in coOrganizers"
|
||||
:key="index"
|
||||
class="organizer-item"
|
||||
>
|
||||
<div class="organizer-content">
|
||||
<div class="organizer-info">
|
||||
<div class="info-row">
|
||||
<span class="label">承办方名称:</span>
|
||||
<span class="value">{{ organizer.name || '未填写' }}</span>
|
||||
<span class="label" v-if="organizer.contact">联系人:</span>
|
||||
<span class="value" v-if="organizer.contact">{{ organizer.contact }}</span>
|
||||
<span class="label" v-if="organizer.phone">联系电话:</span>
|
||||
<span class="value" v-if="organizer.phone">{{ organizer.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="organizer-actions">
|
||||
<el-button
|
||||
type="danger"
|
||||
circle
|
||||
size="small"
|
||||
@click="handleDelete(index)"
|
||||
title="删除"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑承办方对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="currentOrganizer"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="承办方名称" prop="name">
|
||||
<el-input
|
||||
v-model="currentOrganizer.name"
|
||||
placeholder="请输入承办方名称"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="联系人" prop="contact">
|
||||
<el-input
|
||||
v-model="currentOrganizer.contact"
|
||||
placeholder="请输入联系人姓名"
|
||||
maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input
|
||||
v-model="currentOrganizer.phone"
|
||||
placeholder="请输入联系电话"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
import { CoOrganizerInfo } from '@/api/stgl/activity/types';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
|
||||
interface Props {
|
||||
modelValue: CoOrganizerInfo[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: CoOrganizerInfo[]): void;
|
||||
(e: 'change', value: CoOrganizerInfo[]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => []
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const editIndex = ref(-1);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const coOrganizers = ref<CoOrganizerInfo[]>([...props.modelValue]);
|
||||
|
||||
// 当前编辑的承办方
|
||||
const currentOrganizer = ref<CoOrganizerInfo>({
|
||||
name: '',
|
||||
contact: '',
|
||||
phone: ''
|
||||
});
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = computed(() => {
|
||||
return editIndex.value >= 0 ? '编辑承办方' : '添加承办方';
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入承办方名称', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '承办方名称长度在1到100个字符', trigger: 'blur' }
|
||||
],
|
||||
|
||||
contact: [
|
||||
{ max: 50, message: '联系人姓名长度不能超过50个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ max: 20, message: '联系电话长度不能超过20个字符', trigger: 'blur' },
|
||||
{ pattern: /^[0-9\-\s\+\(\)]*$/, message: '请输入正确的电话号码格式', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
// 监听props变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
coOrganizers.value = [...newValue];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听内部数据变化,同步到父组件
|
||||
watch(
|
||||
coOrganizers,
|
||||
(newValue) => {
|
||||
emit('update:modelValue', newValue);
|
||||
emit('change', newValue);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 添加承办方
|
||||
const handleAdd = () => {
|
||||
editIndex.value = -1;
|
||||
currentOrganizer.value = {
|
||||
name: '',
|
||||
contact: '',
|
||||
phone: ''
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 删除承办方
|
||||
const handleDelete = (index: number) => {
|
||||
ElMessageBox.confirm('确定要删除这个承办方吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
coOrganizers.value.splice(index, 1);
|
||||
ElMessage.success('删除成功');
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
};
|
||||
|
||||
// 确认添加/编辑
|
||||
const handleConfirm = () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
// 保存当前操作类型和数据,避免状态重置后丢失
|
||||
const isEdit = editIndex.value >= 0;
|
||||
const currentData = { ...currentOrganizer.value };
|
||||
const currentIndex = editIndex.value;
|
||||
|
||||
// 立即关闭弹窗,避免后续操作影响弹窗状态
|
||||
dialogVisible.value = false;
|
||||
|
||||
// 重置编辑状态
|
||||
editIndex.value = -1;
|
||||
|
||||
// 重置表单
|
||||
formRef.value?.resetFields();
|
||||
|
||||
// 延迟一帧更新数据,确保弹窗已完全关闭
|
||||
setTimeout(() => {
|
||||
if (isEdit) {
|
||||
// 编辑模式
|
||||
coOrganizers.value[currentIndex] = currentData;
|
||||
ElMessage.success('编辑成功');
|
||||
} else {
|
||||
// 添加模式
|
||||
coOrganizers.value.push(currentData);
|
||||
ElMessage.success('添加成功');
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
// 立即关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
|
||||
// 重置编辑状态
|
||||
editIndex.value = -1;
|
||||
|
||||
// 重置表单
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.co-organizer-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.organizer-list {
|
||||
space-y: 12px;
|
||||
}
|
||||
|
||||
.organizer-item {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
background-color: #fafafa;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.organizer-item:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.organizer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.organizer-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.organizer-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="instructor-manager">
|
||||
<div class="header">
|
||||
<span class="title">指导老师信息</span>
|
||||
<el-button type="primary" size="small" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加指导老师
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="instructors.length === 0" class="empty-state">
|
||||
<el-empty description="暂无指导老师信息" :image-size="80" />
|
||||
</div>
|
||||
|
||||
<div v-else class="instructor-list">
|
||||
<div
|
||||
v-for="(instructor, index) in instructors"
|
||||
:key="index"
|
||||
class="instructor-item"
|
||||
>
|
||||
<div class="instructor-content">
|
||||
<div class="instructor-info">
|
||||
<div class="info-row">
|
||||
<span class="label">老师姓名:</span>
|
||||
<span class="value">{{ instructor.name || '未填写' }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="instructor.dept">
|
||||
<span class="label">所属部门:</span>
|
||||
<span class="value">{{ instructor.dept }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="instructor.phone">
|
||||
<span class="label">联系电话:</span>
|
||||
<span class="value">{{ instructor.phone }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="instructor.email">
|
||||
<span class="label">邮箱:</span>
|
||||
<span class="value">{{ instructor.email }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="instructor.hours">
|
||||
<span class="label">指导学时:</span>
|
||||
<span class="value">{{ instructor.hours }} 学时</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instructor-actions">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(index)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑指导老师对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="currentInstructor"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="老师姓名" prop="name">
|
||||
<el-input
|
||||
v-model="currentInstructor.name"
|
||||
placeholder="请输入老师姓名"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属部门" prop="dept">
|
||||
<el-input
|
||||
v-model="currentInstructor.dept"
|
||||
placeholder="请输入所属部门"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input
|
||||
v-model="currentInstructor.phone"
|
||||
placeholder="请输入联系电话"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="currentInstructor.email"
|
||||
placeholder="请输入邮箱地址"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="指导学时" prop="hours">
|
||||
<el-input-number
|
||||
v-model="currentInstructor.hours"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="请输入指导学时"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue';
|
||||
import { InstructorInfo } from '@/api/stgl/activity/types';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
|
||||
interface Props {
|
||||
modelValue: InstructorInfo[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: InstructorInfo[]): void;
|
||||
(e: 'change', value: InstructorInfo[]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => []
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const editIndex = ref(-1);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const instructors = ref<InstructorInfo[]>([...props.modelValue]);
|
||||
|
||||
// 当前编辑的指导老师
|
||||
const currentInstructor = ref<InstructorInfo>({
|
||||
name: '',
|
||||
dept: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
hours: undefined
|
||||
});
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = computed(() => {
|
||||
return editIndex.value >= 0 ? '编辑指导老师' : '添加指导老师';
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入老师姓名', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '老师姓名长度在1到50个字符', trigger: 'blur' }
|
||||
],
|
||||
dept: [
|
||||
{ max: 100, message: '所属部门长度不能超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ max: 20, message: '联系电话长度不能超过20个字符', trigger: 'blur' },
|
||||
{ pattern: /^[0-9\-\s\+\(\)]*$/, message: '请输入正确的电话号码格式', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ max: 100, message: '邮箱长度不能超过100个字符', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
hours: [
|
||||
{ type: 'number', min: 0, max: 9999, message: '指导学时必须在0到9999之间', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
// 监听props变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
instructors.value = [...newValue];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听内部数据变化,同步到父组件
|
||||
watch(
|
||||
instructors,
|
||||
(newValue) => {
|
||||
emit('update:modelValue', newValue);
|
||||
emit('change', newValue);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 添加指导老师
|
||||
const handleAdd = () => {
|
||||
editIndex.value = -1;
|
||||
currentInstructor.value = {
|
||||
name: '',
|
||||
dept: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
hours: undefined
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑指导老师
|
||||
const handleEdit = (index: number) => {
|
||||
editIndex.value = index;
|
||||
currentInstructor.value = { ...instructors.value[index] };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除指导老师
|
||||
const handleDelete = (index: number) => {
|
||||
ElMessageBox.confirm('确定要删除这位指导老师吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
instructors.value.splice(index, 1);
|
||||
ElMessage.success('删除成功');
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
};
|
||||
|
||||
// 确认添加/编辑
|
||||
const handleConfirm = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (editIndex.value >= 0) {
|
||||
// 编辑模式
|
||||
instructors.value[editIndex.value] = { ...currentInstructor.value };
|
||||
ElMessage.success('编辑成功');
|
||||
} else {
|
||||
// 添加模式
|
||||
instructors.value.push({ ...currentInstructor.value });
|
||||
ElMessage.success('添加成功');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
dialogVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.instructor-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.instructor-list {
|
||||
space-y: 12px;
|
||||
}
|
||||
|
||||
.instructor-item {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
background-color: #fafafa;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.instructor-item:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.instructor-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.instructor-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
min-width: 80px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.instructor-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="material-type-selector">
|
||||
<div class="type-options">
|
||||
<el-checkbox-group v-model="selectedTypes" @change="handleChange">
|
||||
<el-checkbox
|
||||
v-for="option in materialTypes"
|
||||
:key="option.value"
|
||||
:label="option.value"
|
||||
class="type-checkbox"
|
||||
>
|
||||
{{ option.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
(e: 'change', value: string): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { stgl_material_type } = toRefs<any>(
|
||||
proxy?.useDict('stgl_material_type')
|
||||
);
|
||||
|
||||
const selectedTypes = ref<string[]>([]);
|
||||
|
||||
// 材料类型选项
|
||||
const materialTypes = computed(() => {
|
||||
return stgl_material_type.value || [];
|
||||
});
|
||||
|
||||
/** 处理选择变化 */
|
||||
const handleChange = (value: string[]) => {
|
||||
const result = value.join(',');
|
||||
emit('update:modelValue', result);
|
||||
emit('change', result);
|
||||
};
|
||||
|
||||
/** 监听外部值变化 */
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal) {
|
||||
selectedTypes.value = newVal.split(',').filter(item => item.trim());
|
||||
} else {
|
||||
selectedTypes.value = [];
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.material-type-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.type-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.type-checkbox {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
padding-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<div class="student-selector">
|
||||
<!-- 学生选择输入框 -->
|
||||
<el-input v-model="displayValue" :placeholder="placeholder" :disabled="disabled" clearable @input="handleSearch" @clear="handleClear">
|
||||
<template #suffix>
|
||||
<el-icon class="search-icon" @click="openDialog">
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 搜索建议下拉 -->
|
||||
<div v-if="showSuggestions && suggestions.length > 0" class="suggestions-dropdown">
|
||||
<div v-for="student in suggestions" :key="student.id" class="suggestion-item" @click="selectStudent(student)">
|
||||
<div class="student-info">
|
||||
<span class="student-name">{{ student.name }}</span>
|
||||
<span class="student-no">{{ student.studentNo }}</span>
|
||||
</div>
|
||||
<div class="student-detail">{{ student.collegeName }} - {{ student.majorName }} - {{ student.className }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 学生选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="选择学生"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
:append-to-body="true"
|
||||
:modal-append-to-body="true"
|
||||
destroy-on-close
|
||||
:z-index="5000"
|
||||
>
|
||||
<div class="selector-content">
|
||||
<!-- 左侧树形结构 -->
|
||||
<div class="tree-panel">
|
||||
<el-tree ref="treeRef" :data="treeData" :props="treeProps" node-key="id" :expand-on-click-node="false" @node-click="handleTreeNodeClick">
|
||||
<template #default="{ node, data }">
|
||||
<span class="tree-node">
|
||||
<span>{{ node.label }}</span>
|
||||
<span v-if="data.studentCount" class="student-count">({{ data.studentCount }}人)</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<!-- 右侧学生列表 -->
|
||||
<div class="student-panel">
|
||||
<div v-if="!selectedClassId" class="empty-tip">请选择左侧班级查看学生列表</div>
|
||||
<div v-else>
|
||||
<div class="panel-header">
|
||||
<span>{{ selectedClassName }} - 学生列表</span>
|
||||
<el-input v-model="studentSearchText" placeholder="搜索学生姓名或学号" size="small" style="width: 200px" clearable>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="student-list">
|
||||
<div
|
||||
v-for="student in filteredStudents"
|
||||
:key="student.id"
|
||||
class="student-item"
|
||||
:class="{ selected: selectedStudent?.id === student.id }"
|
||||
@click="selectCurrentStudent(student)"
|
||||
>
|
||||
<div class="student-info">
|
||||
<span class="student-name">{{ student.name }}</span>
|
||||
<span class="student-no">{{ student.studentNo }}</span>
|
||||
</div>
|
||||
<div class="student-detail">性别:{{ student.gender === '0' ? '男' : '女' }} | 电话:{{ student.phone || '暂无' }}</div>
|
||||
</div>
|
||||
<div v-if="filteredStudents.length === 0" class="empty-tip">暂无学生数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedStudent" @click="confirmSelect"> 确定 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { searchStudentByName, getStudentTreeData, getStudentsByClassId } from '@/api/xg/student';
|
||||
import { StudentVO, StudentTreeNode } from '@/api/xg/student/types';
|
||||
|
||||
interface Props {
|
||||
modelValue: StudentVO | null;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
showStudentNo?: boolean; // 是否在显示名称中包含学号,默认为true
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: StudentVO | null): void;
|
||||
(e: 'change', value: StudentVO | null): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: '请选择学生',
|
||||
disabled: false,
|
||||
showStudentNo: true
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 响应式数据
|
||||
const displayValue = ref('');
|
||||
const suggestions = ref<StudentVO[]>([]);
|
||||
const showSuggestions = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const treeData = ref<StudentTreeNode[]>([]);
|
||||
const selectedClassId = ref<string | number>('');
|
||||
const selectedClassName = ref('');
|
||||
const classStudents = ref<StudentVO[]>([]);
|
||||
const studentSearchText = ref('');
|
||||
const selectedStudent = ref<StudentVO | null>(null);
|
||||
const searchTimer = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 计算属性
|
||||
const filteredStudents = computed(() => {
|
||||
if (!studentSearchText.value) return classStudents.value;
|
||||
const searchText = studentSearchText.value.toLowerCase();
|
||||
return classStudents.value.filter(
|
||||
(student) => student.name.toLowerCase().includes(searchText) || student.studentNo.toLowerCase().includes(searchText)
|
||||
);
|
||||
});
|
||||
|
||||
// 树形结构配置
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
};
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
displayValue.value = props.showStudentNo ? `${newValue.name}(${newValue.studentNo})` : newValue.name;
|
||||
} else {
|
||||
displayValue.value = '';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 搜索学生
|
||||
const handleSearch = (value: string) => {
|
||||
if (searchTimer.value) {
|
||||
clearTimeout(searchTimer.value);
|
||||
}
|
||||
|
||||
if (!value.trim()) {
|
||||
suggestions.value = [];
|
||||
showSuggestions.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimer.value = setTimeout(async () => {
|
||||
try {
|
||||
const response = await searchStudentByName(value.trim());
|
||||
suggestions.value = response.data;
|
||||
showSuggestions.value = true;
|
||||
} catch (error) {
|
||||
console.error('搜索学生失败:', error);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 选择搜索建议中的学生
|
||||
const selectStudent = (student: StudentVO) => {
|
||||
displayValue.value = props.showStudentNo ? `${student.name}(${student.studentNo})` : student.name;
|
||||
suggestions.value = [];
|
||||
showSuggestions.value = false;
|
||||
emit('update:modelValue', student);
|
||||
emit('change', student);
|
||||
};
|
||||
|
||||
// 清除选择
|
||||
const handleClear = () => {
|
||||
suggestions.value = [];
|
||||
showSuggestions.value = false;
|
||||
emit('update:modelValue', null);
|
||||
emit('change', null);
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
if (props.disabled) return;
|
||||
|
||||
// 防止重复打开
|
||||
if (dialogVisible.value) return;
|
||||
|
||||
// 先隐藏建议下拉,避免冲突
|
||||
showSuggestions.value = false;
|
||||
|
||||
// 在TabPane环境下添加延迟,避免闪烁
|
||||
const isInTabPane = document.querySelector('.el-tab-pane[aria-hidden="false"]');
|
||||
|
||||
if (isInTabPane) {
|
||||
// 增加延迟时间,确保TabPane状态稳定
|
||||
setTimeout(() => {
|
||||
if (!dialogVisible.value) {
|
||||
// 再次检查状态
|
||||
dialogVisible.value = true;
|
||||
nextTick(() => {
|
||||
loadTreeData();
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
nextTick(() => {
|
||||
dialogVisible.value = true;
|
||||
loadTreeData();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载树形数据
|
||||
const loadTreeData = async () => {
|
||||
selectedStudent.value = props.modelValue || null;
|
||||
|
||||
// 加载树形数据
|
||||
if (treeData.value.length === 0) {
|
||||
try {
|
||||
const response = await getStudentTreeData();
|
||||
treeData.value = response.data;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载学院专业信息失败');
|
||||
console.error('加载树形数据失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 树节点点击
|
||||
const handleTreeNodeClick = async (data: StudentTreeNode) => {
|
||||
if (data.type === 'class') {
|
||||
selectedClassId.value = data.id;
|
||||
selectedClassName.value = data.label;
|
||||
studentSearchText.value = '';
|
||||
|
||||
try {
|
||||
const response = await getStudentsByClassId(data.id);
|
||||
classStudents.value = response.data;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载学生列表失败');
|
||||
console.error('加载学生列表失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 选择当前学生
|
||||
const selectCurrentStudent = (student: StudentVO) => {
|
||||
selectedStudent.value = student;
|
||||
};
|
||||
|
||||
// 关闭弹窗并清理状态
|
||||
const closeDialog = () => {
|
||||
// 立即设置为false,避免延迟导致的问题
|
||||
dialogVisible.value = false;
|
||||
|
||||
// 强制触发响应式更新,确保UI状态同步
|
||||
nextTick(() => {
|
||||
if (dialogVisible.value !== false) {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const confirmSelect = () => {
|
||||
if (selectedStudent.value) {
|
||||
displayValue.value = props.showStudentNo ? `${selectedStudent.value.name}(${selectedStudent.value.studentNo})` : selectedStudent.value.name;
|
||||
|
||||
// 先触发事件,让父组件处理数据更新
|
||||
emit('update:modelValue', selectedStudent.value);
|
||||
emit('change', selectedStudent.value);
|
||||
|
||||
// 使用nextTick确保所有响应式更新完成后再关闭弹窗
|
||||
nextTick(() => {
|
||||
closeDialog();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 清理弹窗状态(不设置dialogVisible)
|
||||
const cleanupDialogState = () => {
|
||||
selectedClassId.value = '';
|
||||
selectedClassName.value = '';
|
||||
selectedStudent.value = null;
|
||||
classStudents.value = [];
|
||||
studentSearchText.value = '';
|
||||
};
|
||||
|
||||
// 监听弹窗关闭事件
|
||||
watch(dialogVisible, (newVal) => {
|
||||
if (!newVal) {
|
||||
// 延迟清理状态,避免关闭动画期间的闪烁
|
||||
setTimeout(() => {
|
||||
cleanupDialogState();
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// 全局点击事件处理
|
||||
const handleGlobalClick = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// 只在建议下拉显示时才处理点击外部事件
|
||||
if (showSuggestions.value && !target.closest('.student-selector')) {
|
||||
showSuggestions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件生命周期
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleGlobalClick);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
// 清理定时器
|
||||
if (searchTimer.value) {
|
||||
clearTimeout(searchTimer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.student-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-icon:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.suggestions-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f5f7fa;
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.student-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.student-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.student-no {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.student-detail {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.selector-content {
|
||||
display: flex;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.tree-panel {
|
||||
width: 300px;
|
||||
border-right: 1px solid #ebeef5;
|
||||
padding-right: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.student-panel {
|
||||
flex: 1;
|
||||
padding-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.student-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.student-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.student-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.student-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.student-item.selected {
|
||||
background: #e7f4ff;
|
||||
border: 1px solid #b3d8ff;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<div class="teacher-selector">
|
||||
<!-- 教师选择输入框 -->
|
||||
<el-input v-model="displayValue" :placeholder="placeholder" :disabled="disabled" clearable @input="handleSearch" @clear="handleClear">
|
||||
<template #suffix>
|
||||
<el-icon class="search-icon" @click="openDialog">
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 搜索建议下拉 -->
|
||||
<div v-if="showSuggestions && suggestions.length > 0" class="suggestions-dropdown">
|
||||
<div v-for="teacher in suggestions" :key="teacher.userId" class="suggestion-item" @click="selectTeacher(teacher)">
|
||||
<div class="teacher-info">
|
||||
<span class="teacher-name">{{ teacher.nickName }}</span>
|
||||
<span class="teacher-phone">{{ teacher.phonenumber || '暂无电话' }}</span>
|
||||
</div>
|
||||
<div class="teacher-detail">
|
||||
{{ teacher.deptName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 教师选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="选择教师"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
:append-to-body="true"
|
||||
:modal-append-to-body="true"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="selector-content">
|
||||
<!-- 左侧部门树形结构 -->
|
||||
<div class="tree-panel">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
:expand-on-click-node="false"
|
||||
:default-expand-all="true"
|
||||
@node-click="handleTreeNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span class="tree-node">
|
||||
<span>{{ node.label }}</span>
|
||||
<span v-if="data.teacherCount" class="teacher-count">({{ data.teacherCount }}人)</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<!-- 右侧教师列表 -->
|
||||
<div class="teacher-panel">
|
||||
<div v-if="!selectedDeptId" class="empty-tip">请选择左侧部门查看教师列表</div>
|
||||
<div v-else>
|
||||
<div class="panel-header">
|
||||
<span>{{ selectedDeptName }} - 教师列表</span>
|
||||
<el-input v-model="teacherSearchText" placeholder="搜索教师姓名或账号" size="small" style="width: 200px" clearable>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="teacher-list">
|
||||
<div
|
||||
v-for="teacher in filteredTeachers"
|
||||
:key="teacher.userId"
|
||||
class="teacher-item"
|
||||
:class="{ selected: selectedTeacher?.userId === teacher.userId }"
|
||||
@click="selectCurrentTeacher(teacher)"
|
||||
>
|
||||
<div class="teacher-info">
|
||||
<span class="teacher-name">{{ teacher.nickName }}</span>
|
||||
<span class="teacher-account">{{ teacher.userName }}</span>
|
||||
</div>
|
||||
<div class="teacher-detail">
|
||||
性别:{{ teacher.sex === '0' ? '男' : teacher.sex === '1' ? '女' : '未知' }} | 电话:{{ teacher.phonenumber || '暂无' }} | 邮箱:{{
|
||||
teacher.email || '暂无'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredTeachers.length === 0" class="empty-tip">暂无教师数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedTeacher" @click="confirmSelect"> 确定 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { searchTeacherByName, getDeptTreeData, getTeachersByDeptId } from '@/api/system/teacher';
|
||||
import { TeacherVO, DeptTreeNode } from '@/api/system/teacher/types';
|
||||
|
||||
interface Props {
|
||||
modelValue?: TeacherVO | null;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: TeacherVO | null): void;
|
||||
(e: 'change', value: TeacherVO | null): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: '请输入教师姓名或点击搜索图标选择',
|
||||
disabled: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 响应式数据
|
||||
const displayValue = ref('');
|
||||
const suggestions = ref<TeacherVO[]>([]);
|
||||
const showSuggestions = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const treeRef = ref();
|
||||
const treeData = ref<DeptTreeNode[]>([]);
|
||||
const selectedDeptId = ref<string | number>('');
|
||||
const selectedDeptName = ref('');
|
||||
const deptTeachers = ref<TeacherVO[]>([]);
|
||||
const teacherSearchText = ref('');
|
||||
const selectedTeacher = ref<TeacherVO | null>(null);
|
||||
const searchTimer = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 计算属性
|
||||
const filteredTeachers = computed(() => {
|
||||
if (!teacherSearchText.value) return deptTeachers.value;
|
||||
const searchText = teacherSearchText.value.toLowerCase();
|
||||
return deptTeachers.value.filter(
|
||||
(teacher) => teacher.nickName.toLowerCase().includes(searchText) || teacher.userName.toLowerCase().includes(searchText)
|
||||
);
|
||||
});
|
||||
|
||||
// 树形结构配置
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
};
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
displayValue.value = newValue.nickName;
|
||||
} else {
|
||||
displayValue.value = '';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 搜索教师
|
||||
const handleSearch = (value: string) => {
|
||||
if (searchTimer.value) {
|
||||
clearTimeout(searchTimer.value);
|
||||
}
|
||||
|
||||
if (!value.trim()) {
|
||||
suggestions.value = [];
|
||||
showSuggestions.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimer.value = setTimeout(async () => {
|
||||
try {
|
||||
const response = await searchTeacherByName(value.trim());
|
||||
suggestions.value = response.data;
|
||||
showSuggestions.value = true;
|
||||
} catch (error) {
|
||||
console.error('搜索教师失败:', error);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 选择搜索建议中的教师
|
||||
const selectTeacher = (teacher: TeacherVO) => {
|
||||
console.log('TeacherSelector - selectTeacher:', teacher);
|
||||
displayValue.value = teacher.nickName;
|
||||
suggestions.value = [];
|
||||
showSuggestions.value = false;
|
||||
emit('update:modelValue', teacher);
|
||||
emit('change', teacher);
|
||||
};
|
||||
|
||||
// 清除选择
|
||||
const handleClear = () => {
|
||||
suggestions.value = [];
|
||||
showSuggestions.value = false;
|
||||
emit('update:modelValue', null);
|
||||
emit('change', null);
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
// 防止重复打开
|
||||
if (dialogVisible.value) return;
|
||||
|
||||
// 先隐藏建议下拉,避免冲突
|
||||
showSuggestions.value = false;
|
||||
|
||||
// 在TabPane环境下添加延迟,避免闪烁
|
||||
const isInTabPane = document.querySelector('.el-tab-pane[aria-hidden="false"]');
|
||||
|
||||
if (isInTabPane) {
|
||||
// 增加延迟时间,确保TabPane状态稳定
|
||||
setTimeout(() => {
|
||||
if (!dialogVisible.value) {
|
||||
// 再次检查状态
|
||||
dialogVisible.value = true;
|
||||
nextTick(() => {
|
||||
loadDeptTree();
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
nextTick(() => {
|
||||
dialogVisible.value = true;
|
||||
loadDeptTree();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载部门树形数据
|
||||
const loadDeptTree = async () => {
|
||||
try {
|
||||
const response = await getDeptTreeData();
|
||||
treeData.value = response.data;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载部门信息失败');
|
||||
console.error('加载树形数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 树节点点击
|
||||
const handleTreeNodeClick = async (data: DeptTreeNode) => {
|
||||
selectedDeptId.value = data.id;
|
||||
selectedDeptName.value = data.label;
|
||||
teacherSearchText.value = '';
|
||||
|
||||
try {
|
||||
const response = await getTeachersByDeptId(data.id);
|
||||
deptTeachers.value = response.data;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载教师列表失败');
|
||||
console.error('加载教师列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择当前教师
|
||||
const selectCurrentTeacher = (teacher: TeacherVO) => {
|
||||
selectedTeacher.value = teacher;
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const confirmSelect = () => {
|
||||
if (selectedTeacher.value) {
|
||||
console.log('TeacherSelector - confirmSelect:', selectedTeacher.value);
|
||||
displayValue.value = selectedTeacher.value.nickName;
|
||||
emit('update:modelValue', selectedTeacher.value);
|
||||
emit('change', selectedTeacher.value);
|
||||
closeDialog();
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗并清理状态
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
// 延迟清理状态,避免关闭动画期间的闪烁
|
||||
setTimeout(() => {
|
||||
selectedDeptId.value = '';
|
||||
selectedDeptName.value = '';
|
||||
selectedTeacher.value = null;
|
||||
deptTeachers.value = [];
|
||||
teacherSearchText.value = '';
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 监听弹窗关闭事件
|
||||
watch(dialogVisible, (newVal) => {
|
||||
if (!newVal) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
// 全局点击事件处理器
|
||||
const handleGlobalClick = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// 只有在建议下拉显示时才处理,避免干扰弹窗
|
||||
if (showSuggestions.value && !target.closest('.teacher-selector')) {
|
||||
showSuggestions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时添加事件监听器
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleGlobalClick);
|
||||
});
|
||||
|
||||
// 组件卸载时移除事件监听器
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
if (searchTimer.value) {
|
||||
clearTimeout(searchTimer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.teacher-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-icon:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.suggestions-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f5f7fa;
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.teacher-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.teacher-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.teacher-phone,
|
||||
.teacher-account {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.teacher-detail {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.selector-content {
|
||||
display: flex;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.tree-panel {
|
||||
width: 300px;
|
||||
border-right: 1px solid #ebeef5;
|
||||
padding-right: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.teacher-panel {
|
||||
flex: 1;
|
||||
padding-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.teacher-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.teacher-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.teacher-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.teacher-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.teacher-item.selected {
|
||||
background: #e7f4ff;
|
||||
border: 1px solid #b3d8ff;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="team-member-manager">
|
||||
<div class="member-list">
|
||||
<div v-for="(member, index) in memberList" :key="index" class="member-item">
|
||||
<el-card shadow="hover" class="member-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>成员 {{ index + 1 }}</span>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
icon="Delete"
|
||||
circle
|
||||
@click="removeMember(index)"
|
||||
:disabled="memberList.length <= 1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="member" label-width="80px" size="small">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="姓名" required>
|
||||
<el-input v-model="member.studentName" placeholder="请输入姓名" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学号" required>
|
||||
<el-input v-model="member.studentNo" placeholder="请输入学号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="性别">
|
||||
<el-select v-model="member.gender" placeholder="请选择性别" style="width: 100%">
|
||||
<el-option label="男" value="0" />
|
||||
<el-option label="女" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="member.phone" placeholder="请输入联系电话" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="院系">
|
||||
<el-input v-model="member.department" placeholder="请输入院系" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="专业">
|
||||
<el-input v-model="member.major" placeholder="请输入专业" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="班级">
|
||||
<el-input v-model="member.className" placeholder="请输入班级" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="member.email" placeholder="请输入邮箱" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="add-member">
|
||||
<el-button type="primary" icon="Plus" @click="addMember">添加成员</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="TeamMemberManager" lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import type { TeamMemberInfo } from '@/api/stgl/activityregisterhall/types';
|
||||
|
||||
interface Props {
|
||||
modelValue: TeamMemberInfo[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: TeamMemberInfo[]): void;
|
||||
(e: 'change', value: TeamMemberInfo[]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => []
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 成员列表
|
||||
const memberList = ref<TeamMemberInfo[]>([]);
|
||||
|
||||
// 初始化成员列表
|
||||
const initMemberList = () => {
|
||||
if (props.modelValue && props.modelValue.length > 0) {
|
||||
memberList.value = [...props.modelValue];
|
||||
} else {
|
||||
// 默认至少有一个成员
|
||||
memberList.value = [createEmptyMember()];
|
||||
}
|
||||
};
|
||||
|
||||
// 创建空成员对象
|
||||
const createEmptyMember = (): TeamMemberInfo => ({
|
||||
userId: '',
|
||||
studentNo: '',
|
||||
studentName: '',
|
||||
gender: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
department: '',
|
||||
major: '',
|
||||
className: '',
|
||||
isLeader: 0
|
||||
});
|
||||
|
||||
// 添加成员
|
||||
const addMember = () => {
|
||||
memberList.value.push(createEmptyMember());
|
||||
emitChange();
|
||||
};
|
||||
|
||||
// 删除成员
|
||||
const removeMember = (index: number) => {
|
||||
if (memberList.value.length > 1) {
|
||||
memberList.value.splice(index, 1);
|
||||
emitChange();
|
||||
}
|
||||
};
|
||||
|
||||
// 发送变更事件
|
||||
const emitChange = () => {
|
||||
emit('update:modelValue', memberList.value);
|
||||
emit('change', memberList.value);
|
||||
};
|
||||
|
||||
// 监听成员列表变化
|
||||
watch(
|
||||
memberList,
|
||||
() => {
|
||||
emitChange();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听外部传入的值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (JSON.stringify(newValue) !== JSON.stringify(memberList.value)) {
|
||||
initMemberList();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 初始化
|
||||
nextTick(() => {
|
||||
initMemberList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.team-member-manager {
|
||||
.member-list {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.member-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.member-card {
|
||||
border-radius: 8px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-member {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
border-top: 1px dashed #e4e7ed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<div class="vice-president-manager">
|
||||
<div class="manager-header">
|
||||
<span class="title">{{ title }}</span>
|
||||
<el-button type="primary" size="small" @click="addVicePresident">
|
||||
<el-icon><Plus /></el-icon>
|
||||
{{ addButtonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="vicePresidents.length === 0" class="empty-tip">{{ emptyTip }}</div>
|
||||
|
||||
<div v-else class="vice-president-list">
|
||||
<div v-for="(vp, index) in vicePresidents" :key="index" class="vice-president-item">
|
||||
<el-row :gutter="10" align="middle">
|
||||
<el-col :span="4">
|
||||
<el-form-item label="姓名" :prop="`vicePresidents.${index}.name`">
|
||||
<StudentSelector
|
||||
v-model="vp.student"
|
||||
:placeholder="selectPlaceholder"
|
||||
:showStudentNo="false"
|
||||
@change="(student) => handleStudentChange(index, student)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="联系方式" :prop="`vicePresidents.${index}.phone`">
|
||||
<el-input v-model="vp.phone" placeholder="联系方式" readonly />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="院系" :prop="`vicePresidents.${index}.dept`">
|
||||
<el-input v-model="vp.dept" placeholder="院系" readonly />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="专业" :prop="`vicePresidents.${index}.major`">
|
||||
<el-input v-model="vp.major" placeholder="专业" readonly />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="班级" :prop="`vicePresidents.${index}.class`">
|
||||
<el-input v-model="vp.class" placeholder="班级" readonly />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col
|
||||
:span="1"
|
||||
style="text-align: center; display: flex; align-items: flex-start; justify-content: center; margin-top: -14px; margin-left: 10px"
|
||||
>
|
||||
<el-button type="danger" size="small" circle @click="removeVicePresident(index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
import StudentSelector from '@/components/StudentSelector/index.vue';
|
||||
import { StudentVO } from '@/api/xg/student/types';
|
||||
|
||||
export interface VicePresidentVO {
|
||||
student: StudentVO | null;
|
||||
name: string;
|
||||
dept: string;
|
||||
major: string;
|
||||
class: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: VicePresidentVO[];
|
||||
title?: string;
|
||||
addButtonText?: string;
|
||||
emptyTip?: string;
|
||||
selectPlaceholder?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: VicePresidentVO[]): void;
|
||||
(e: 'change', value: VicePresidentVO[]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '副社长管理',
|
||||
addButtonText: '添加副社长',
|
||||
emptyTip: '暂无副社长,点击上方按钮添加',
|
||||
selectPlaceholder: '选择副社长'
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const vicePresidents = ref<VicePresidentVO[]>([]);
|
||||
|
||||
// 防止递归更新的锁
|
||||
const updateLock = ref(false);
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (updateLock.value) return;
|
||||
updateLock.value = true;
|
||||
vicePresidents.value = [...newValue];
|
||||
nextTick(() => {
|
||||
updateLock.value = false;
|
||||
});
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// 监听内部值变化,发出change事件给父组件
|
||||
watch(
|
||||
vicePresidents,
|
||||
(newValue) => {
|
||||
if (updateLock.value) return;
|
||||
updateLock.value = true;
|
||||
emit('change', newValue);
|
||||
nextTick(() => {
|
||||
updateLock.value = false;
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 添加副社长
|
||||
const addVicePresident = () => {
|
||||
vicePresidents.value.push({
|
||||
student: null,
|
||||
name: '',
|
||||
dept: '',
|
||||
major: '',
|
||||
class: '',
|
||||
phone: ''
|
||||
});
|
||||
};
|
||||
|
||||
// 删除副社长
|
||||
const removeVicePresident = (index: number) => {
|
||||
vicePresidents.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 学生选择变化
|
||||
const handleStudentChange = (index: number, student: StudentVO | null) => {
|
||||
const vp = vicePresidents.value[index];
|
||||
if (student) {
|
||||
vp.student = student;
|
||||
vp.name = student.name;
|
||||
vp.dept = student.collegeName;
|
||||
vp.major = student.majorName;
|
||||
vp.class = student.className;
|
||||
vp.phone = student.phone;
|
||||
} else {
|
||||
vp.student = null;
|
||||
vp.name = '';
|
||||
vp.dept = '';
|
||||
vp.major = '';
|
||||
vp.class = '';
|
||||
vp.phone = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vice-president-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.manager-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vice-president-list {
|
||||
space-y: 16px;
|
||||
}
|
||||
|
||||
.vice-president-item {
|
||||
padding: 16px 8px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 让姓名、联系方式、院系等字段往左移,保持宽度一致 */
|
||||
.vice-president-item :deep(.el-col:first-child .el-form-item) {
|
||||
margin-left: -65px !important;
|
||||
}
|
||||
|
||||
.vice-president-item :deep(.el-col:nth-child(2) .el-form-item) {
|
||||
margin-left: -50px !important;
|
||||
}
|
||||
|
||||
.vice-president-item :deep(.el-col:nth-child(3) .el-form-item) {
|
||||
margin-left: -75px !important;
|
||||
}
|
||||
|
||||
.vice-president-item :deep(.el-col:nth-child(4) .el-form-item) {
|
||||
margin-left: -75px !important;
|
||||
}
|
||||
|
||||
.vice-president-item :deep(.el-col:nth-child(5) .el-form-item) {
|
||||
margin-left: -75px !important;
|
||||
}
|
||||
|
||||
/* 备用选择器,确保样式生效 */
|
||||
.vice-president-item .el-col:nth-child(2) .el-form-item {
|
||||
margin-left: -50px !important;
|
||||
}
|
||||
|
||||
.vice-president-item .el-col:nth-child(3) .el-form-item {
|
||||
margin-left: -75px !important;
|
||||
}
|
||||
|
||||
.vice-president-item .el-col:nth-child(4) .el-form-item {
|
||||
margin-left: -75px !important;
|
||||
}
|
||||
|
||||
.vice-president-item .el-col:nth-child(5) .el-form-item {
|
||||
margin-left: -75px !important;
|
||||
}
|
||||
|
||||
.vice-president-item:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.vice-president-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user