wuzhen更新社团管理新需求

This commit is contained in:
“wuzhen” committed 2026-05-22 11:17:26 +08:00
1 parent 52d892337e
commit 0745d287f5
32 files changed
+1638 -76

No files matched your search

@@ -98,4 +98,13 @@ public class StglClassController extends BaseController {
List<StglClassVo> list = stglClassService.queryList(bo);
ExcelUtil.exportExcel(list, "班级学生参与情况", StglClassVo.class, response);
}
/**
* 查询班主任所辖班级中当前正在参加活动的学生列表
*/
//@SaCheckPermission("stgl:stclass:list")
@GetMapping("/current-activities")
public TableDataInfo<Map<String, Object>> currentActivities(StglClassBo bo, PageQuery pageQuery) {
return stglClassService.getCurrentActiveStudents(bo, pageQuery);
}
}
@@ -29,6 +29,9 @@ import org.dromara.stgl.domain.vo.StglMemberVo;
import org.dromara.stgl.domain.vo.StglMemberImportVo;
import org.dromara.stgl.domain.bo.StglMemberBo;
import org.dromara.stgl.service.IStglMemberService;
import org.dromara.stgl.service.IStglMemberApprovalService;
import org.dromara.stgl.domain.bo.StglMemberApprovalBo;
import org.dromara.stgl.domain.vo.StglMemberApprovalVo;
import org.dromara.stgl.listener.StglMemberImportListener;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import io.swagger.v3.oas.annotations.Operation;
@@ -51,6 +54,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
public class StglMemberController extends BaseController {
private final IStglMemberService stglMemberService;
private final IStglMemberApprovalService memberApprovalService;
/**
* 查询社团成员列表
@@ -232,4 +236,44 @@ public class StglMemberController extends BaseController {
ExcelUtil.exportExcel(templateData, "社团成员导入模板", StglMemberImportVo.class, response);
}
/**
* 社长审批成员申请
*/
@Operation(summary = "社长审批成员申请")
@Log(title = "社长审批", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/presidentApprove")
public R<Void> presidentApprove(@RequestBody StglMemberApprovalBo bo) {
return toAjax(memberApprovalService.presidentApprove(bo.getMemberId(), bo.getApproved(), bo.getOpinion()));
}
/**
* 指导老师审批成员申请
*/
@Operation(summary = "指导老师审批成员申请")
@Log(title = "指导老师审批", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/advisorApprove")
public R<Void> advisorApprove(@RequestBody StglMemberApprovalBo bo) {
return toAjax(memberApprovalService.advisorApprove(bo.getMemberId(), bo.getApproved(), bo.getOpinion()));
}
/**
* 获取成员申请的审批流程
*/
@Operation(summary = "获取成员申请的审批流程")
@GetMapping("/approvalFlow/{memberId}")
public R<List<StglMemberApprovalVo>> getApprovalFlow(@PathVariable Long memberId) {
return R.ok(memberApprovalService.getApprovalFlow(memberId));
}
/**
* 获取待当前用户审批的成员列表
*/
@Operation(summary = "获取待当前用户审批的成员列表")
@GetMapping("/pendingApprovals")
public R<List<StglMemberApprovalVo>> getPendingApprovals(@RequestParam(required = false) Long clubId) {
return R.ok(memberApprovalService.getPendingApprovals(clubId));
}
}
@@ -152,6 +152,11 @@ public class StglClub extends TenantEntity {
*/
private String advisorsJson;
/**
* 归属(部门ID)
*/
private Long affiliation;
/**
* 成员规模上限
*/
@@ -197,6 +202,21 @@ public class StglClub extends TenantEntity {
*/
private String sighOpen;
/**
* 社长用户ID
*/
private Long presidentUserId;
/**
* 物资保管员用户ID
*/
private Long materialKeeperUserId;
/**
* 物资保管员姓名
*/
private String materialKeeperName;
/**
* 备注
*/
@@ -0,0 +1,57 @@
package org.dromara.stgl.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import org.dromara.common.tenant.core.TenantEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.Date;
/**
* 成员申请审批流程对象 stgl_member_approval
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("stgl_member_approval")
public class StglMemberApproval extends TenantEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "id")
private Long id;
/** 成员记录ID(关联stgl_member) */
private Long memberId;
/** 社团ID */
private Long clubId;
/** 节点代码(president_approve/advisor_approve) */
private String nodeCode;
/** 节点名称(社长审批/指导老师审批) */
private String nodeName;
/** 节点顺序(1.0, 2.0) */
private BigDecimal nodeOrder;
/** 审批人ID */
private Long approverId;
/** 审批人姓名 */
private String approverName;
/** 审批结果(通过/不通过) */
private String approveResult;
/** 审批状态(pending/approved/rejected) */
private String auditStatus;
/** 审批意见 */
private String auditOpinion;
/** 审批时间 */
private Date auditTime;
}
@@ -12,6 +12,7 @@ import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.stgl.domain.StglActivity;
import java.util.Date;
import java.util.List;
/**
* 活动信息业务对象 stgl_activity
@@ -229,4 +230,8 @@ public class StglActivityBo extends BaseEntity {
*/
private String remark;
}
/**
* 邀请成员ID列表(非数据库字段,用于创建报名记录)
*/
private List<Long> invitedMemberIds;
}
@@ -184,6 +184,11 @@ public class StglClubBo extends BaseEntity {
*/
private String advisorsJson;
/**
* 归属(部门ID)
*/
private Long affiliation;
/**
* 成员规模上限
*/
@@ -240,6 +245,22 @@ public class StglClubBo extends BaseEntity {
*/
private String remark;
/**
* 社长用户ID(创建时选择学生)
*/
private Long presidentUserId;
/**
* 物资保管员用户ID(创建时选择学生)
*/
private Long materialKeeperUserId;
/**
* 物资保管员姓名
*/
@Size(max = 50, message = "物资保管员姓名长度不能超过50个字符")
private String materialKeeperName;
/**
* 社团ID列表(用于数据权限控制)
*/
@@ -0,0 +1,22 @@
package org.dromara.stgl.domain.bo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 成员审批请求对象
*/
@Data
public class StglMemberApprovalBo {
/** 成员记录ID */
@NotNull(message = "成员记录ID不能为空")
private Long memberId;
/** 是否通过 */
@NotNull(message = "审批结果不能为空")
private Boolean approved;
/** 审批意见 */
private String opinion;
}
@@ -178,6 +178,11 @@ public class StglClubVo implements Serializable {
*/
private String advisorsJson;
/**
* 归属(部门ID)
*/
private Long affiliation;
/**
* 成员规模上限
*/
@@ -232,6 +237,22 @@ public class StglClubVo implements Serializable {
@ExcelProperty(value = "开启报名")
private String sighOpen;
/**
* 社长用户ID
*/
private Long presidentUserId;
/**
* 物资保管员用户ID
*/
private Long materialKeeperUserId;
/**
* 物资保管员姓名
*/
@ExcelProperty(value = "物资保管员")
private String materialKeeperName;
/**
* 备注
*/
@@ -0,0 +1,35 @@
package org.dromara.stgl.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 成员申请审批流程视图对象
*/
@Data
public class StglMemberApprovalVo implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long memberId;
private Long clubId;
private String nodeCode;
private String nodeName;
private BigDecimal nodeOrder;
private Long approverId;
private String approverName;
private String approveResult;
private String auditStatus;
private String auditOpinion;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date auditTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -62,4 +62,20 @@ public interface StglClassMapper extends BaseMapperPlus<StglClass, StglClassVo>
* @return 活动详情列表
*/
List<Map<String, Object>> selectStudentActivities(Long studentId);
/**
* 查询指定班级中当前正在参加活动的学生列表
*
* @param params 查询参数(classId, studentName, studentNo, offset, pageSize)
* @return 正在参加活动的学生列表
*/
List<Map<String, Object>> selectCurrentActiveStudents(Map<String, Object> params);
/**
* 查询指定班级中当前正在参加活动的学生总数
*
* @param params 查询参数(classId, studentName, studentNo)
* @return 学生总数
*/
Integer selectCurrentActiveStudentsCount(Map<String, Object> params);
}
@@ -0,0 +1,11 @@
package org.dromara.stgl.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.stgl.domain.StglMemberApproval;
import org.dromara.stgl.domain.vo.StglMemberApprovalVo;
/**
* 成员申请审批流程Mapper接口
*/
public interface StglMemberApprovalMapper extends BaseMapperPlus<StglMemberApproval, StglMemberApprovalVo> {
}
@@ -65,4 +65,13 @@ public interface IStglClassService {
* @return 活动详情列表
*/
List<Map<String, Object>> getStudentActivities(Long studentId);
/**
* 查询班主任所辖班级中当前正在参加活动的学生分页列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 分页结果
*/
TableDataInfo<Map<String, Object>> getCurrentActiveStudents(StglClassBo bo, PageQuery pageQuery);
}
@@ -0,0 +1,56 @@
package org.dromara.stgl.service;
import org.dromara.stgl.domain.vo.StglMemberApprovalVo;
import java.util.List;
/**
* 成员申请审批流程Service接口
*/
public interface IStglMemberApprovalService {
/**
* 为成员申请创建审批流程(社长审批 → 指导老师审批)
*
* @param memberId 成员记录ID
* @param clubId 社团ID
* @return 是否创建成功
*/
Boolean createApprovalFlow(Long memberId, Long clubId);
/**
* 获取成员申请的审批流程列表
*
* @param memberId 成员记录ID
* @return 审批节点列表
*/
List<StglMemberApprovalVo> getApprovalFlow(Long memberId);
/**
* 获取待当前用户审批的列表
*
* @param clubId 社团ID(可选)
* @return 待审批的成员列表
*/
List<StglMemberApprovalVo> getPendingApprovals(Long clubId);
/**
* 社长审批
*
* @param memberId 成员记录ID
* @param approved 是否通过
* @param opinion 审批意见
* @return 是否审批成功
*/
Boolean presidentApprove(Long memberId, Boolean approved, String opinion);
/**
* 指导老师审批
*
* @param memberId 成员记录ID
* @param approved 是否通过
* @param opinion 审批意见
* @return 是否审批成功
*/
Boolean advisorApprove(Long memberId, Boolean approved, String opinion);
}
@@ -47,10 +47,13 @@ import org.dromara.common.json.utils.JsonUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import org.dromara.stgl.domain.StglClub;
import org.dromara.stgl.domain.StglVolunteerTeam;
import org.dromara.stgl.domain.StglActivityRegistration;
import org.dromara.stgl.domain.StglMember;
import org.dromara.stgl.domain.vo.StglClubVo;
import org.dromara.stgl.domain.vo.StglVolunteerTeamVo;
import org.dromara.stgl.mapper.StglClubMapper;
import org.dromara.stgl.mapper.StglVolunteerTeamMapper;
import org.dromara.stgl.mapper.StglActivityRegistrationMapper;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.service.ISysUserService;
@@ -76,6 +79,7 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
private final IStglActivityAuditService activityAuditService;
private final StglActivityAuditMapper activityAuditMapper;
private final IStglClubService clubService;
private final StglActivityRegistrationMapper registrationMapper;
/**
* 查询活动信息
@@ -283,8 +287,8 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
// 为指导老师分配角色
assignAdvisorRoles(add);
createInvitedRegistrations(add.getId(), bo.getInvitedMemberIds());
}
return flag;
}
@@ -311,8 +315,8 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
boolean updateResult = baseMapper.updateById(update) > 0;
if (updateResult) {
// 处理指导老师角色分配
handleAdvisorRoleAssignment(oldActivity, update);
createInvitedRegistrations(update.getId(), bo.getInvitedMemberIds());
// 监听状态变化:如果状态从0(草稿)变为8(已提交),自动创建审批记录
if (oldActivity != null &&
@@ -1018,4 +1022,37 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
// 注意:submitActivityForApproval 和 withdrawActivityApproval 方法已废弃
// 功能已整合到 updateByBo 方法中,通过监听状态变化自动处理审批记录
}
private void createInvitedRegistrations(Long activityId, List<Long> invitedMemberIds) {
if (invitedMemberIds == null || invitedMemberIds.isEmpty() || activityId == null) {
return;
}
Date now = new Date();
for (Long userId : invitedMemberIds) {
LambdaQueryWrapper<StglActivityRegistration> existLqw = Wrappers.lambdaQuery();
existLqw.eq(StglActivityRegistration::getActivityId, activityId)
.eq(StglActivityRegistration::getUserId, userId);
if (registrationMapper.selectCount(existLqw) > 0) {
continue;
}
StglMember member = stglMemberMapper.selectOne(
Wrappers.<StglMember>lambdaQuery()
.eq(StglMember::getUserId, userId)
.eq(StglMember::getStatus, 2)
.last("LIMIT 1")
);
StglActivityRegistration reg = new StglActivityRegistration();
reg.setActivityId(activityId);
reg.setUserId(userId);
reg.setStudentNo(member != null ? member.getStudentNo() : "");
reg.setStudentName(member != null ? member.getStudentName() : "");
reg.setRegistrationType("1");
reg.setRegistrationTime(now);
reg.setApprovalStatus("1");
reg.setApprovalTime(now);
reg.setRemark("活动邀请自动报名");
registrationMapper.insert(reg);
}
log.info("活动邀请报名完成: activityId={}, 邀请人数={}", activityId, invitedMemberIds.size());
}
}
@@ -115,6 +115,23 @@ public class StglClassServiceImpl implements IStglClassService {
return baseMapper.selectStudentActivities(studentId);
}
@Override
public TableDataInfo<Map<String, Object>> getCurrentActiveStudents(StglClassBo bo, PageQuery pageQuery) {
Map<String, Object> params = buildQueryParams(bo);
params.put("offset", (pageQuery.getPageNum() - 1) * pageQuery.getPageSize());
params.put("pageSize", pageQuery.getPageSize());
List<Map<String, Object>> list = baseMapper.selectCurrentActiveStudents(params);
Map<String, Object> countParams = buildQueryParams(bo);
Integer total = baseMapper.selectCurrentActiveStudentsCount(countParams);
TableDataInfo<Map<String, Object>> result = new TableDataInfo<>();
result.setRows(list);
result.setTotal(total != null ? total : 0);
return result;
}
/**
* 构建查询参数
*
@@ -15,6 +15,7 @@ import org.dromara.stgl.domain.StglMember;
import org.dromara.stgl.mapper.StglClubMapper;
import org.dromara.stgl.mapper.StglMemberMapper;
import org.dromara.stgl.service.IStglClubRegisterService;
import org.dromara.stgl.service.IStglMemberApprovalService;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.stgl.domain.vo.StglClubVo;
import org.dromara.system.domain.SysUser;
@@ -40,6 +41,7 @@ public class StglClubRegisterServiceImpl implements IStglClubRegisterService {
private final StglMemberMapper memberMapper;
private final SysUserMapper sysUserMapper;
private final XgStudentMsgMapper xgStudentMsgMapper;
private final IStglMemberApprovalService memberApprovalService;
/**
* 分页查询社团报名大厅列表
@@ -247,10 +249,15 @@ public class StglClubRegisterServiceImpl implements IStglClubRegisterService {
member.setStudentName(studentName != null ? studentName : user.getNickName()); // 如果没有学生信息,使用用户昵称
// 申请时不设置roleType,等待审核通过后再设置
member.setJoinTime(new Date());
member.setStatus(0); // 待审核状态(stgl_member_status字典,新报名需审核)
member.setRemark(reason); // 将申请理由存储到备注字段
return memberMapper.insert(member) > 0;
member.setStatus(0); // 待审核状态
member.setRemark(reason);
boolean inserted = memberMapper.insert(member) > 0;
if (inserted) {
// 创建两级审批流程:社长审批 → 指导老师审批
memberApprovalService.createApprovalFlow(member.getId(), clubId);
}
return inserted;
}
/**
@@ -334,23 +334,162 @@ public class StglClubServiceImpl implements IStglClubService {
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(StglClubBo bo) {
Long currentUserId = LoginHelper.getUserId();
if (currentUserId == null) {
throw new RuntimeException("用户未登录");
}
StglClub add = MapstructUtils.convert(bo, StglClub.class);
// 根据社长用户ID从学生表查询并填充冗余字段
if (add.getPresidentUserId() != null) {
fillPresidentInfoFromStudent(add);
}
// 创建者自动设为首席指导老师
add.setCreateBy(currentUserId);
if (StringUtils.isBlank(add.getAdvisorName())) {
// 从用户表获取创建者(老师)信息填充指导老师字段
fillCreatorAsAdvisor(add, currentUserId);
}
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
// 自动创建社长和副社长的成员记录
createLeaderMemberRecords(add);
// 为指导老师分配角色
createMaterialKeeperMemberRecord(add);
assignAdvisorRoles(add);
// 为社长分配角色
assignPresidentRole(add);
// 创建指导老师历史记录
createAdvisorHistoryRecords(add);
}
return flag;
}
/**
* 根据社长用户ID从学生表查询信息,自动填充冗余字段
*/
private void fillPresidentInfoFromStudent(StglClub club) {
try {
LambdaQueryWrapper<XgStudentMsg> wrapper = Wrappers.lambdaQuery();
wrapper.eq(XgStudentMsg::getId, club.getPresidentUserId())
.eq(XgStudentMsg::getDeleted, 0)
.last("LIMIT 1");
XgStudentMsg student = studentMsgMapper.selectOne(wrapper);
if (student != null) {
club.setPresidentName(student.getName());
club.setPresidentStudentNo(student.getStudentNo());
club.setPresidentDept(student.getCollegeName());
club.setPresidentMajor(student.getMajorName());
club.setPresidentClass(student.getClassName());
club.setPresidentPhone(student.getPhone());
}
} catch (Exception e) {
log.error("根据用户ID查询社长学生信息失败: {}", e.getMessage());
}
}
/**
* 将创建者设为首席指导老师
*/
private void fillCreatorAsAdvisor(StglClub club, Long userId) {
try {
String nickName = LoginHelper.getLoginUser().getNickname();
club.setAdvisorName(nickName);
} catch (Exception e) {
log.warn("获取创建者老师信息失败: {}", e.getMessage());
}
}
/**
* 创建物资保管员成员记录
*/
private void createMaterialKeeperMemberRecord(StglClub club) {
if (club.getMaterialKeeperUserId() == null) {
return;
}
try {
LambdaQueryWrapper<XgStudentMsg> wrapper = Wrappers.lambdaQuery();
wrapper.eq(XgStudentMsg::getId, club.getMaterialKeeperUserId())
.eq(XgStudentMsg::getDeleted, 0)
.last("LIMIT 1");
XgStudentMsg student = studentMsgMapper.selectOne(wrapper);
if (student == null) {
log.warn("物资保管员学生信息不存在, userId={}", club.getMaterialKeeperUserId());
return;
}
StglMember keeper = new StglMember();
keeper.setMemberType(1);
keeper.setRelationId(club.getId());
keeper.setUserId(club.getMaterialKeeperUserId());
keeper.setStudentNo(student.getStudentNo());
keeper.setStudentName(student.getName());
keeper.setRoleType("物资保管员");
keeper.setJoinTime(new Date());
keeper.setStatus(2);
keeper.setRemark("社团创建时自动添加");
memberMapper.insert(keeper);
// 回填物资保管员姓名到社团表
if (StringUtils.isBlank(club.getMaterialKeeperName())) {
StglClub updateClub = new StglClub();
updateClub.setId(club.getId());
updateClub.setMaterialKeeperName(student.getName());
baseMapper.updateById(updateClub);
}
} catch (Exception e) {
log.error("创建物资保管员成员记录失败: {}", e.getMessage());
}
}
private void syncMaterialKeeperMemberRecord(StglClub newClub, StglClub oldClub) {
Long newKeeperId = newClub.getMaterialKeeperUserId();
Long oldKeeperId = oldClub != null ? oldClub.getMaterialKeeperUserId() : null;
if (java.util.Objects.equals(newKeeperId, oldKeeperId)) {
return;
}
// 删除旧的物资保管员成员记录
if (oldKeeperId != null) {
LambdaQueryWrapper<StglMember> delWrapper = Wrappers.lambdaQuery();
delWrapper.eq(StglMember::getRelationId, newClub.getId())
.eq(StglMember::getMemberType, 1)
.eq(StglMember::getRoleType, "物资保管员");
memberMapper.delete(delWrapper);
}
// 创建新的物资保管员成员记录
if (newKeeperId != null) {
LambdaQueryWrapper<XgStudentMsg> wrapper = Wrappers.lambdaQuery();
wrapper.eq(XgStudentMsg::getId, newKeeperId)
.eq(XgStudentMsg::getDeleted, 0)
.last("LIMIT 1");
XgStudentMsg student = studentMsgMapper.selectOne(wrapper);
if (student != null) {
StglMember keeper = new StglMember();
keeper.setMemberType(1);
keeper.setRelationId(newClub.getId());
keeper.setUserId(newKeeperId);
keeper.setStudentNo(student.getStudentNo());
keeper.setStudentName(student.getName());
keeper.setRoleType("物资保管员");
keeper.setJoinTime(new Date());
keeper.setStatus(2);
keeper.setRemark("修改社团信息时更新");
memberMapper.insert(keeper);
if (StringUtils.isBlank(newClub.getMaterialKeeperName())) {
StglClub updateName = new StglClub();
updateName.setId(newClub.getId());
updateName.setMaterialKeeperName(student.getName());
baseMapper.updateById(updateName);
}
}
}
}
/**
* 修改社团信息
*
@@ -375,6 +514,8 @@ public class StglClubServiceImpl implements IStglClubService {
if (updateResult) {
// 同步更新副社长的成员记录
syncVicePresidentsInMembers(update);
// 同步物资保管员成员记录
syncMaterialKeeperMemberRecord(update, oldClub);
// 处理指导老师角色分配
handleAdvisorRoleAssignment(oldClub, update);
// 更新指导老师历史记录
@@ -522,25 +663,35 @@ public class StglClubServiceImpl implements IStglClubService {
*/
private void createLeaderMemberRecords(StglClub club) {
Date now = new Date();
// 1. 创建社长成员记录
if (StringUtils.isNotBlank(club.getPresidentStudentNo())) {
// 1. 创建社长成员记录(优先用 presidentUserId)
if (club.getPresidentUserId() != null) {
StglMember president = new StglMember();
president.setMemberType(1); // 社团成员
president.setMemberType(1);
president.setRelationId(club.getId());
president.setUserId(club.getPresidentUserId());
president.setStudentNo(club.getPresidentStudentNo());
president.setStudentName(club.getPresidentName());
president.setRoleType("社长");
president.setJoinTime(now);
president.setStatus(2);
president.setRemark("社团创建时自动添加");
memberMapper.insert(president);
} else if (StringUtils.isNotBlank(club.getPresidentStudentNo())) {
// 兼容旧逻辑:通过学号查找
StglMember president = new StglMember();
president.setMemberType(1);
president.setRelationId(club.getId());
president.setStudentNo(club.getPresidentStudentNo());
president.setStudentName(club.getPresidentName());
president.setRoleType("社长"); // 社长
president.setRoleType("社长");
president.setJoinTime(now);
president.setStatus(2); // 已加入状态(stgl_member_status字典)
president.setStatus(2);
president.setRemark("社团创建时自动添加");
// 根据学号查找用户ID
Long presidentUserId = findUserIdByStudentNo(club.getPresidentStudentNo());
if (presidentUserId != null) {
president.setUserId(presidentUserId);
}
memberMapper.insert(president);
}
@@ -0,0 +1,262 @@
package org.dromara.stgl.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.stgl.domain.StglClub;
import org.dromara.stgl.domain.StglMember;
import org.dromara.stgl.domain.StglMemberApproval;
import org.dromara.stgl.domain.vo.StglMemberApprovalVo;
import org.dromara.stgl.mapper.StglClubMapper;
import org.dromara.stgl.mapper.StglMemberApprovalMapper;
import org.dromara.stgl.mapper.StglMemberMapper;
import org.dromara.stgl.service.IStglMemberApprovalService;
import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
/**
* 成员申请审批流程Service业务层处理
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class StglMemberApprovalServiceImpl implements IStglMemberApprovalService {
private final StglMemberApprovalMapper approvalMapper;
private final StglMemberMapper memberMapper;
private final StglClubMapper clubMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean createApprovalFlow(Long memberId, Long clubId) {
StglClub club = clubMapper.selectById(clubId);
if (club == null) {
throw new RuntimeException("社团不存在");
}
// 获取社长信息(审批节点1)
Long presidentUserId = club.getPresidentUserId();
String presidentName = club.getPresidentName();
if (presidentUserId == null) {
// 兼容旧数据:从 stgl_member 表查找社长
LambdaQueryWrapper<StglMember> lqw = Wrappers.lambdaQuery();
lqw.eq(StglMember::getMemberType, 1)
.eq(StglMember::getRelationId, clubId)
.eq(StglMember::getRoleType, "社长")
.eq(StglMember::getStatus, 2)
.last("LIMIT 1");
StglMember president = memberMapper.selectOne(lqw);
if (president != null) {
presidentUserId = president.getUserId();
presidentName = president.getStudentName();
}
}
if (presidentUserId == null) {
throw new RuntimeException("社团暂无社长,无法提交申请");
}
// 获取指导老师信息(审批节点2)- 从 advisors_json 中获取首席指导老师
Long advisorUserId = null;
String advisorName = club.getAdvisorName();
String advisorsJson = club.getAdvisorsJson();
if (StringUtils.isNotBlank(advisorsJson)) {
try {
List<Map<String, Object>> advisors = JsonUtils.parseObject(advisorsJson, new TypeReference<List<Map<String, Object>>>() {});
if (advisors != null && !advisors.isEmpty()) {
Map<String, Object> first = advisors.get(0);
Object uid = first.get("userId");
if (uid != null) {
advisorUserId = Long.parseLong(uid.toString());
}
if (first.get("teacher") instanceof Map) {
Map<?, ?> teacher = (Map<?, ?>) first.get("teacher");
if (teacher.get("nickName") != null) {
advisorName = teacher.get("nickName").toString();
}
}
}
} catch (Exception e) {
log.warn("解析advisors_json失败: {}", e.getMessage());
}
}
if (advisorUserId == null) {
advisorUserId = club.getCreateBy();
}
if (advisorUserId == null) {
throw new RuntimeException("社团暂无指导老师,无法提交申请");
}
Date now = new Date();
Long currentUserId = LoginHelper.getUserId();
// 节点1:社长审批
StglMemberApproval presidentNode = new StglMemberApproval();
presidentNode.setMemberId(memberId);
presidentNode.setClubId(clubId);
presidentNode.setNodeCode("president_approve");
presidentNode.setNodeName("社长审批");
presidentNode.setNodeOrder(new BigDecimal("1.0"));
presidentNode.setApproverId(presidentUserId);
presidentNode.setApproverName(presidentName != null ? presidentName : "");
presidentNode.setAuditStatus("pending");
approvalMapper.insert(presidentNode);
// 节点2:指导老师审批
StglMemberApproval advisorNode = new StglMemberApproval();
advisorNode.setMemberId(memberId);
advisorNode.setClubId(clubId);
advisorNode.setNodeCode("advisor_approve");
advisorNode.setNodeName("指导老师审批");
advisorNode.setNodeOrder(new BigDecimal("2.0"));
advisorNode.setApproverId(advisorUserId);
advisorNode.setApproverName(advisorName != null ? advisorName : "");
advisorNode.setAuditStatus("pending");
approvalMapper.insert(advisorNode);
log.info("创建成员审批流程: memberId={}, clubId={}, 社长={}, 指导老师={}",
memberId, clubId, presidentUserId, advisorUserId);
return true;
}
@Override
public List<StglMemberApprovalVo> getApprovalFlow(Long memberId) {
LambdaQueryWrapper<StglMemberApproval> lqw = Wrappers.lambdaQuery();
lqw.eq(StglMemberApproval::getMemberId, memberId)
.orderByAsc(StglMemberApproval::getNodeOrder);
return approvalMapper.selectVoList(lqw);
}
@Override
public List<StglMemberApprovalVo> getPendingApprovals(Long clubId) {
Long currentUserId = LoginHelper.getUserId();
LambdaQueryWrapper<StglMemberApproval> lqw = Wrappers.lambdaQuery();
lqw.eq(StglMemberApproval::getApproverId, currentUserId)
.eq(StglMemberApproval::getAuditStatus, "pending");
if (clubId != null) {
lqw.eq(StglMemberApproval::getClubId, clubId);
}
lqw.orderByAsc(StglMemberApproval::getNodeOrder);
return approvalMapper.selectVoList(lqw);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean presidentApprove(Long memberId, Boolean approved, String opinion) {
Boolean result = doApprove(memberId, "president_approve", approved, opinion);
if (approved) {
StglMember member = memberMapper.selectById(memberId);
if (member != null) {
StglMember updateMember = new StglMember();
updateMember.setId(memberId);
updateMember.setStatus(1); // 审核中,等待指导老师审批
memberMapper.updateById(updateMember);
log.info("社长审批通过,进入指导老师审批阶段: memberId={}", memberId);
}
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean advisorApprove(Long memberId, Boolean approved, String opinion) {
Boolean result = doApprove(memberId, "advisor_approve", approved, opinion);
// 指导老师审批是最后一个节点,如果通过则更新成员状态为已加入
if (approved) {
StglMember member = memberMapper.selectById(memberId);
if (member != null) {
StglMember updateMember = new StglMember();
updateMember.setId(memberId);
updateMember.setStatus(2); // 已加入
updateMember.setRoleType("普通成员");
memberMapper.updateById(updateMember);
log.info("成员申请全部审批通过,已加入社团: memberId={}", memberId);
}
}
return result;
}
/**
* 通用审批处理逻辑
*/
private Boolean doApprove(Long memberId, String nodeCode, Boolean approved, String opinion) {
Long currentUserId = LoginHelper.getUserId();
if (currentUserId == null) {
throw new RuntimeException("用户未登录");
}
// 查找待审批的节点
LambdaQueryWrapper<StglMemberApproval> lqw = Wrappers.lambdaQuery();
lqw.eq(StglMemberApproval::getMemberId, memberId)
.eq(StglMemberApproval::getNodeCode, nodeCode)
.eq(StglMemberApproval::getAuditStatus, "pending");
StglMemberApproval approvalNode = approvalMapper.selectOne(lqw);
if (approvalNode == null) {
throw new RuntimeException("未找到待审批记录或已审批过");
}
// 校验审批人身份
log.info("审批人校验: approverId={}, currentUserId={}, equals={}",
approvalNode.getApproverId(), currentUserId,
approvalNode.getApproverId().equals(currentUserId));
log.info("审批人校验类型: approverId.class={}, currentUserId.class={}",
approvalNode.getApproverId().getClass().getName(),
currentUserId.getClass().getName());
if (!approvalNode.getApproverId().equals(currentUserId)) {
throw new RuntimeException("您不是该节点的审批人(节点审批人:" + approvalNode.getApproverId() + ", 当前用户:" + currentUserId + ")");
}
// 更新审批记录
StglMemberApproval updateNode = new StglMemberApproval();
updateNode.setId(approvalNode.getId());
updateNode.setApproveResult(approved ? "通过" : "不通过");
updateNode.setAuditStatus(approved ? "approved" : "rejected");
updateNode.setAuditOpinion(opinion);
updateNode.setAuditTime(new Date());
approvalMapper.updateById(updateNode);
// 如果拒绝,更新成员状态为已拒绝,并标记后续节点也结束
if (!approved) {
StglMember member = memberMapper.selectById(memberId);
if (member != null) {
StglMember updateMember = new StglMember();
updateMember.setId(memberId);
updateMember.setStatus(3); // 已拒绝
updateMember.setRemark(opinion != null ? "审批拒绝: " + opinion : "审批拒绝");
memberMapper.updateById(updateMember);
}
// 将后续待审批节点也标记为结束
LambdaQueryWrapper<StglMemberApproval> pendingLqw = Wrappers.lambdaQuery();
pendingLqw.eq(StglMemberApproval::getMemberId, memberId)
.eq(StglMemberApproval::getAuditStatus, "pending")
.ne(StglMemberApproval::getId, approvalNode.getId());
List<StglMemberApproval> pendingNodes = approvalMapper.selectList(pendingLqw);
for (StglMemberApproval pending : pendingNodes) {
StglMemberApproval cancelNode = new StglMemberApproval();
cancelNode.setId(pending.getId());
cancelNode.setAuditStatus("rejected");
cancelNode.setApproveResult("前置节点拒绝,自动终止");
cancelNode.setAuditTime(new Date());
approvalMapper.updateById(cancelNode);
}
log.info("成员申请被拒绝: memberId={}, nodeCode={}, 拒绝人={}", memberId, nodeCode, currentUserId);
} else {
log.info("成员申请节点审批通过: memberId={}, nodeCode={}, 审批人={}", memberId, nodeCode, currentUserId);
}
return true;
}
}
@@ -16,6 +16,7 @@ import org.dromara.stgl.domain.vo.StglMemberVo;
import org.dromara.stgl.domain.StglMember;
import org.dromara.stgl.mapper.StglMemberMapper;
import org.dromara.stgl.service.IStglMemberService;
import org.dromara.stgl.service.IStglMemberApprovalService;
import org.dromara.system.service.ISysUserService;
import cn.hutool.core.collection.CollUtil;
import org.dromara.common.satoken.utils.LoginHelper;
@@ -44,6 +45,7 @@ public class StglMemberServiceImpl implements IStglMemberService {
private final StglMemberMapper baseMapper;
private final ISysUserService userService;
private final IStglMemberApprovalService memberApprovalService;
/**
* 查询社团成员
@@ -381,28 +383,25 @@ public class StglMemberServiceImpl implements IStglMemberService {
if (member == null) {
throw new RuntimeException("成员记录不存在");
}
if (member.getStatus() != 0) {
if (member.getStatus() != 0 && member.getStatus() != 1) {
throw new RuntimeException("该申请已审核过,无法重复审核");
}
// 更新成员状态和角色
StglMember updateMember = new StglMember();
updateMember.setId(id);
if (approved) {
updateMember.setStatus(2); // 已加入状态(stgl_member_status字典)
// 根据成员类型设置不同的角色
if (member.getMemberType() == 1) {
updateMember.setRoleType("普通成员"); // 社团成员
} else if (member.getMemberType() == 2) {
updateMember.setRoleType("队员"); // 志愿者队伍成员
Long currentUserId = LoginHelper.getUserId();
java.util.Set<String> roleKeys = LoginHelper.getLoginUser().getRolePermission();
if (roleKeys.contains("stgl_instructor")) {
if (member.getStatus() == 0) {
throw new RuntimeException("该申请需等待社长先审批,请稍后再试");
}
return memberApprovalService.advisorApprove(id, approved, null);
} else {
updateMember.setStatus(3); // 已驳回状态(stgl_member_status字典)
updateMember.setRoleType(null); // 拒绝时清空角色
if (member.getStatus() == 1) {
throw new RuntimeException("社长已审批通过,等待指导老师审批中");
}
return memberApprovalService.presidentApprove(id, approved, null);
}
return baseMapper.updateById(updateMember) > 0;
}
/**
@@ -31,6 +31,7 @@
<result property="advisorEmail" column="advisor_email"/>
<result property="vicePresidentsJson" column="vice_presidents_json"/>
<result property="advisorsJson" column="advisors_json"/>
<result property="affiliation" column="affiliation"/>
<result property="memberLimit" column="member_limit"/>
<result property="departments" column="departments"/>
<result property="recruitCycle" column="recruit_cycle"/>
@@ -56,6 +57,7 @@
c.vice_presidents_json,
c.advisor_name, c.advisor_dept, c.advisor_phone, c.advisor_email,
c.advisors_json,
c.affiliation,
c.member_limit, c.departments, c.recruit_cycle, c.recruit_time, c.activity_frequency, c.funding_sources,
c.charter_url, c.status, c.sigh_open, c.remark, c.create_by, c.create_time, c.update_by, c.update_time, c.tenant_id, c.deleted,
COALESCE(m.member_count, 0) as memberCount
@@ -76,6 +78,7 @@
c.president_dept, c.president_major, c.president_class, c.president_phone,
c.vice_president_name, c.vice_president_student_no, c.vice_president_dept, c.vice_president_major, c.vice_president_class, c.vice_president_phone,
c.advisor_name, c.advisor_dept, c.advisor_phone, c.advisor_email,
c.affiliation,
c.member_limit, c.departments, c.recruit_cycle, c.recruit_time,
c.activity_frequency, c.funding_sources, c.charter_url, c.status, c.sigh_open, c.remark,
c.create_by, c.create_time, c.update_by, c.update_time,
@@ -98,6 +101,7 @@
c.president_dept, c.president_major, c.president_class, c.president_phone,
c.vice_president_name, c.vice_president_student_no, c.vice_president_dept, c.vice_president_major, c.vice_president_class, c.vice_president_phone,
c.advisor_name, c.advisor_dept, c.advisor_phone, c.advisor_email,
c.affiliation,
c.member_limit, c.departments, c.recruit_cycle, c.recruit_time,
c.activity_frequency, c.funding_sources, c.charter_url, c.status, c.sigh_open, c.remark,
c.create_by, c.create_time, c.update_by, c.update_time,
@@ -129,6 +133,7 @@
c.vice_presidents_json,
c.advisor_name, c.advisor_dept, c.advisor_phone, c.advisor_email,
c.advisors_json,
c.affiliation,
c.member_limit, c.departments, c.recruit_cycle, c.recruit_time,
c.activity_frequency, c.funding_sources, c.charter_url, c.status, c.sigh_open, c.remark,
c.create_by, c.create_time, c.update_by, c.update_time,
@@ -445,4 +445,132 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ORDER BY a.start_time DESC, a.create_time DESC
</select>
<!-- 查询指定班级中当前正在参加活动的学生列表(个人报名 + 团队报名) -->
<select id="selectCurrentActiveStudents" parameterType="java.util.Map" resultType="java.util.Map">
SELECT
s.id,
s.student_no AS studentNo,
s.name AS studentName,
s.gender,
s.phone,
c.class_name AS className,
a.id AS activityId,
a.activity_name AS activityName,
a.start_time AS startTime,
a.end_time AS endTime,
a.location,
CASE
WHEN a.related_type = 1 THEN club.club_name
WHEN a.related_type = 2 THEN vt.team_name
ELSE NULL
END AS organizationName
FROM xg_student_msg s
LEFT JOIN xg_class_msg c ON s.class_id = c.id AND c.tenant_id = '000000'
INNER JOIN (
SELECT ar.user_id, ar.activity_id FROM stgl_activity_registration ar
INNER JOIN stgl_activity a ON ar.activity_id = a.id AND a.deleted = 0 AND a.tenant_id = '000000'
WHERE ar.deleted = 0 AND ar.approval_status = 1 AND ar.tenant_id = '000000'
AND NOW() BETWEEN a.start_time AND a.end_time
UNION
SELECT
CAST(JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) AS UNSIGNED) AS user_id,
ati.activity_id
FROM stgl_activity_team_info ati
CROSS JOIN (
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION
SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION
SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION
SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19
) numbers
INNER JOIN stgl_activity a ON ati.activity_id = a.id AND a.deleted = 0 AND a.tenant_id = '000000'
WHERE ati.deleted = 0 AND ati.tenant_id = '000000'
AND NOW() BETWEEN a.start_time AND a.end_time
AND JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) IS NOT NULL
AND JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) != ''
AND EXISTS (
SELECT 1 FROM stgl_activity_registration team_ar
WHERE team_ar.team_id = ati.id
AND team_ar.deleted = 0
AND team_ar.approval_status = 1
AND team_ar.registration_type = 2
AND team_ar.tenant_id = '000000'
)
) active ON s.id = active.user_id
INNER JOIN stgl_activity a ON active.activity_id = a.id AND a.deleted = 0 AND a.tenant_id = '000000'
LEFT JOIN stgl_club club ON a.related_type = 1 AND a.related_club_id = club.id AND club.deleted = 0 AND club.tenant_id = '000000'
LEFT JOIN stgl_volunteer_team vt ON a.related_type = 2 AND a.related_club_id = vt.id AND vt.deleted = 0 AND vt.tenant_id = '000000'
<where>
s.deleted = 0
AND s.student_status = '1'
AND s.tenant_id = '000000'
<if test="classId != null and classId != ''">
AND s.class_id = #{classId}
</if>
<if test="studentName != null and studentName != ''">
AND s.name LIKE CONCAT('%', #{studentName}, '%')
</if>
<if test="studentNo != null and studentNo != ''">
AND s.student_no LIKE CONCAT('%', #{studentNo}, '%')
</if>
</where>
ORDER BY a.start_time, s.student_no
<if test="offset != null and pageSize != null">
LIMIT #{pageSize} OFFSET #{offset}
</if>
</select>
<!-- 查询指定班级中当前正在参加活动的学生总数 -->
<select id="selectCurrentActiveStudentsCount" parameterType="java.util.Map" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM xg_student_msg s
INNER JOIN (
SELECT ar.user_id, ar.activity_id FROM stgl_activity_registration ar
INNER JOIN stgl_activity a ON ar.activity_id = a.id AND a.deleted = 0 AND a.tenant_id = '000000'
WHERE ar.deleted = 0 AND ar.approval_status = 1 AND ar.tenant_id = '000000'
AND NOW() BETWEEN a.start_time AND a.end_time
UNION
SELECT
CAST(JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) AS UNSIGNED) AS user_id,
ati.activity_id
FROM stgl_activity_team_info ati
CROSS JOIN (
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION
SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION
SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION
SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19
) numbers
INNER JOIN stgl_activity a ON ati.activity_id = a.id AND a.deleted = 0 AND a.tenant_id = '000000'
WHERE ati.deleted = 0 AND ati.tenant_id = '000000'
AND NOW() BETWEEN a.start_time AND a.end_time
AND JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) IS NOT NULL
AND JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) != ''
AND EXISTS (
SELECT 1 FROM stgl_activity_registration team_ar
WHERE team_ar.team_id = ati.id
AND team_ar.deleted = 0
AND team_ar.approval_status = 1
AND team_ar.registration_type = 2
AND team_ar.tenant_id = '000000'
)
) active ON s.id = active.user_id
<where>
s.deleted = 0
AND s.student_status = '1'
AND s.tenant_id = '000000'
<if test="classId != null and classId != ''">
AND s.class_id = #{classId}
</if>
<if test="studentName != null and studentName != ''">
AND s.name LIKE CONCAT('%', #{studentName}, '%')
</if>
<if test="studentNo != null and studentNo != ''">
AND s.student_no LIKE CONCAT('%', #{studentNo}, '%')
</if>
</where>
</select>
</mapper>
+5
View File
@@ -461,6 +461,11 @@ export interface StglActivityForm {
* 备注
*/
remark?: string;
/**
* 邀请成员ID列表(前端专用,提交时传给后端)
*/
invitedMemberIds?: (string | number)[];
}
export interface StglActivityQuery extends PageQuery {
+47 -4
View File
@@ -79,6 +79,21 @@ export interface StglClubVO {
*/
presidentPhone: string;
/**
* 社长用户ID(关联学生用户)
*/
presidentUserId?: string | number;
/**
* 物资保管员用户ID
*/
materialKeeperUserId?: string | number;
/**
* 物资保管员姓名
*/
materialKeeperName?: string;
/**
* 副社长列表
*/
@@ -148,10 +163,7 @@ export interface StglClubVO {
* 指导老师列表JSON格式存储
*/
advisorsJson?: string;
/**
* 成员规模上限
*/
affiliation?: string;
memberLimit: number;
/**
@@ -286,6 +298,36 @@ export interface StglClubForm {
*/
presidentPhone?: string;
/**
* 社长用户ID(关联学生用户)
*/
presidentUserId?: string | number;
/**
* 物资保管员用户ID
*/
materialKeeperUserId?: string | number;
/**
* 物资保管员姓名
*/
materialKeeperName?: string;
/**
* 物资保管员学生对象(前端选择器用)
*/
materialKeeperStudent?: StudentVO | null;
/**
* 物资保管员学号(前端显示用)
*/
materialKeeperStudentNo?: string;
/**
* 物资保管员联系方式(前端显示用)
*/
materialKeeperPhone?: string;
/**
* 副社长列表
*/
@@ -350,6 +392,7 @@ export interface StglClubForm {
* 成员规模上限
*/
memberLimit?: number;
affiliation?: string;
/**
* 组织架构
+36 -4
View File
@@ -98,9 +98,41 @@ export const approveMember = (id: string | number, approved: boolean) => {
return request({
url: `/stgl/member/approve/${id}`,
method: 'put',
params: { approved },
headers: {
isEncrypt: 'true'
}
params: { approved }
});
};
export const presidentApprove = (memberId: number, approved: boolean, opinion?: string) => {
return request({
url: '/stgl/member/presidentApprove',
method: 'put',
data: { memberId, approved, opinion },
headers: { isEncrypt: 'true' }
});
};
export const advisorApprove = (memberId: number, approved: boolean, opinion?: string) => {
return request({
url: '/stgl/member/advisorApprove',
method: 'put',
data: { memberId, approved, opinion },
headers: { isEncrypt: 'true' }
});
};
export const getApprovalFlow = (memberId: number) => {
return request({
url: `/stgl/member/approvalFlow/${memberId}`,
method: 'get',
headers: { isEncrypt: 'true' }
});
};
export const getPendingApprovals = (clubId?: number) => {
return request({
url: '/stgl/member/pendingApprovals',
method: 'get',
params: clubId ? { clubId } : {},
headers: { isEncrypt: 'true' }
});
};
+8
View File
@@ -62,3 +62,11 @@ export function exportStglClass(query: StglClassQuery) {
params: query
});
}
export function getCurrentActivities(query: StglClassQuery) {
return request({
url: '/stgl/stclass/current-activities',
method: 'get',
params: query
});
}
+15 -19
View File
@@ -290,28 +290,24 @@ export interface ActivityDetailVO {
* 班级详情对象
*/
export interface StglClassDetailVO {
/**
* 班级ID
*/
id: string | number;
/**
* 班级名称
*/
className: string;
/**
* 学院名称
*/
collegeName: string;
/**
* 专业名称
*/
majorName: string;
/**
* 年级
*/
grade: string;
}
export interface CurrentActivityVO {
id: string | number;
studentNo: string;
studentName: string;
gender: string;
phone: string;
className: string;
activityId: string | number;
activityName: string;
startTime: string;
endTime: string;
location: string;
organizationName: string;
}
@@ -118,6 +118,7 @@ watch(
(newValue) => {
if (updateLock.value) return;
updateLock.value = true;
emit('update:modelValue', newValue);
emit('change', newValue);
nextTick(() => {
updateLock.value = false;
+77 -2
View File
@@ -610,6 +610,34 @@
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">邀请成员</el-divider>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="邀请成员">
<div style="width: 100%">
<div style="margin-bottom: 10px; display: flex; align-items: center; gap: 12px;">
<el-checkbox v-model="inviteCheckAll" :indeterminate="inviteIndeterminate" @change="handleInviteCheckAll">全选</el-checkbox>
<el-input v-model="inviteSearchText" placeholder="搜索姓名/学号" clearable style="width: 200px" size="small" />
<span style="color: #909399; font-size: 12px;">已选 {{ form.invitedMemberIds?.length || 0 }} / {{ inviteMemberList.length }} 人</span>
<span v-if="!form.relatedClubId" style="color: #E6A23C; font-size: 12px;">请先选择关联社团</span>
</div>
<el-checkbox-group v-model="form.invitedMemberIds" @change="handleInviteChange">
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #dcdfe6; border-radius: 4px; padding: 8px;">
<div v-if="inviteMemberList.length === 0" style="color: #909399; text-align: center; padding: 20px;">暂无可邀请的成员</div>
<el-checkbox
v-for="member in filteredInviteMembers"
:key="member.userId"
:label="member.userId"
style="width: 200px; margin: 4px 0;"
>
{{ member.studentName }}({{ member.studentNo }})
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-form>
@@ -690,7 +718,7 @@
</template>
<script setup name="StglActivity" lang="ts">
import { nextTick, watch } from 'vue';
import { nextTick, watch, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElNotification } from 'element-plus';
import {
@@ -719,6 +747,7 @@ import { AdvisorVO } from '@/components/AdvisorManager/index.vue';
import { useUserStore } from '@/store/modules/user';
import { checkClubActivityPermission, type ClubActivityPermissionResult } from '@/api/stgl/clubPermission';
import { getPendingActivityAudits } from '@/api/stgl/activity/audit';
import { listStglMember } from '@/api/stgl/member';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
@@ -741,6 +770,37 @@ const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
// 邀请成员相关
interface InviteMember { userId: string; studentName: string; studentNo: string; }
const inviteMemberList = ref<InviteMember[]>([]);
const inviteCheckAll = ref(false);
const inviteIndeterminate = ref(false);
const inviteSearchText = ref('');
const filteredInviteMembers = computed(() => {
if (!inviteSearchText.value) return inviteMemberList.value;
const kw = inviteSearchText.value.toLowerCase();
return inviteMemberList.value.filter(m => m.studentName.toLowerCase().includes(kw) || m.studentNo.includes(kw));
});
const handleInviteCheckAll = (val: boolean) => {
form.value.invitedMemberIds = val ? filteredInviteMembers.value.map(m => m.userId) : [];
inviteIndeterminate.value = false;
};
const handleInviteChange = (val: (string | number)[]) => {
const total = filteredInviteMembers.value.length;
inviteCheckAll.value = val.length === total;
inviteIndeterminate.value = val.length > 0 && val.length < total;
};
const loadInviteMembers = async (clubId: string | number) => {
try {
const res = await listStglMember({ memberType: 1, relationId: clubId, status: '2', pageNum: 1, pageSize: 9999 } as any);
const rows = (res as any)?.rows || [];
inviteMemberList.value = rows.map((m: any) => ({ userId: String(m.userId), studentName: m.studentName, studentNo: m.studentNo }));
} catch (e) {
console.error('加载社团成员失败:', e);
inviteMemberList.value = [];
}
};
const multiple = ref(true);
const total = ref(0);
const activeTab = ref('basic');
@@ -836,7 +896,8 @@ const initFormData: StglActivityForm = {
summaryAttachments: undefined,
achievementsAttachments: undefined,
importSource: undefined,
remark: undefined
remark: undefined,
invitedMemberIds: []
};
const data = reactive<PageData<StglActivityForm, StglActivityQuery & { relatedClub?: ClubOption | null; relatedDept?: DeptOption | null }>>({
@@ -2113,6 +2174,20 @@ watch(
},
{ deep: true }
);
watch(
() => form.value.relatedClub,
(newClub) => {
if (newClub?.relatedClubId && newClub.relatedType === 1) {
loadInviteMembers(newClub.relatedClubId);
} else {
inviteMemberList.value = [];
form.value.invitedMemberIds = [];
inviteCheckAll.value = false;
}
},
{ deep: true }
);
</script>
<style scoped>
@@ -59,6 +59,14 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="审批阶段" align="center" width="120">
<template #default="scope">
<el-tag v-if="scope.row.status === 0" type="warning">等待社长审批</el-tag>
<el-tag v-else-if="scope.row.status === 1" type="info">等待指导老师审批</el-tag>
<el-tag v-else-if="scope.row.status === 3" type="danger">已驳回</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="studentName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="60">
@@ -105,6 +113,7 @@
style="font-size: 16px; margin-right: 8px"
@click="handleApprove(scope.row)"
v-hasPermi="['stgl:clubmember:approve']"
v-if="scope.row.status === '0' || scope.row.status === '1'"
></el-button>
</el-tooltip>
</template>
@@ -188,6 +197,7 @@ import { Check, Close } from '@element-plus/icons-vue';
import { listStglMember, getStglMember, approveMember } from '@/api/stgl/member';
import { StglMemberVO, StglMemberQuery } from '@/api/stgl/member/types';
import { parseTime } from '@/utils/ruoyi';
import { useUserStore } from '@/store/modules/user';
interface Props {
clubId: string | number;
@@ -277,12 +287,16 @@ const getList = async () => {
console.log('PendingMembersTab: getList 开始查询,参数:', queryData);
// 待审核页面:查询待审核(status=0)和已驳回(status=3)的成员
// 字典配置:0-待审核,1-审核中,2-已加入,3-已驳回,4-已退出
// 根据角色动态设置查询状态
// 社长:看到 status=0(待审核)+ status=3(已驳回)
// 指导老师:看到 status=1(审核中,社长已通过)+ status=3(已驳回)
const userRoles = useUserStore().roles;
const isInstructor = userRoles.includes('stgl_instructor');
if (!queryData.status) {
// 使用params传递多状态查询
queryData.params = { ...queryData.params, statusList: ['0', '3'] } as any;
delete queryData.status; // 删除单个status字段
const statusList = isInstructor ? ['1', '3'] : ['0', '3'];
queryData.params = { ...queryData.params, statusList } as any;
delete queryData.status;
}
// 确保phone参数正确传递到params中
@@ -439,7 +453,8 @@ const submitApproval = () => {
})
.catch((error) => {
console.error('审核失败:', error);
proxy.$modal.msgError('审核失败,请检查数据完整性');
const msg = error?.msg || error?.message || '审核失败';
proxy.$modal.msgError(msg);
});
} else {
// 批量审核 - 再次验证数据完整性
@@ -461,7 +476,8 @@ const submitApproval = () => {
})
.catch((error) => {
console.error('批量审核失败:', error);
proxy.$modal.msgError('批量审核失败,请检查数据完整性');
const msg = error?.msg || error?.message || '批量审核失败';
proxy.$modal.msgError(msg);
});
}
}
+180 -1
View File
@@ -241,6 +241,13 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属" prop="affiliation">
<el-select v-model="form.affiliation" placeholder="请选择归属" style="width: 100%" :disabled="!hasInstructorPermission">
<el-option v-for="item in affiliationOptions" :key="item.deptId" :label="item.deptName" :value="item.deptId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
@@ -333,6 +340,17 @@
</el-row>
</div>
<!-- 副社长信息 -->
<div class="personnel-section">
<h4 class="section-title">副社长信息</h4>
<VicePresidentManager
v-model="form.vicePresidents"
title=""
add-button-text="添加副社长"
empty-tip="暂未添加副社长"
/>
</div>
<!-- 指导老师信息 -->
<div class="personnel-section">
<h4 class="section-title">指导老师信息</h4>
@@ -343,6 +361,33 @@
:disabled="!hasInstructorPermission"
/>
</div>
<!-- 物资保管员 -->
<div class="personnel-section">
<h4 class="section-title">物资保管员</h4>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="选择学生" prop="materialKeeperStudent">
<StudentSelector
v-model="form.materialKeeperStudent"
placeholder="请选择物资保管员"
:showStudentNo="false"
@change="handleMaterialKeeperChange"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="学号">
<el-input v-model="form.materialKeeperStudentNo" placeholder="学号" readonly />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系方式">
<el-input v-model="form.materialKeeperPhone" placeholder="联系方式" readonly />
</el-form-item>
</el-col>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane label="组织架构" name="organization">
@@ -422,6 +467,7 @@
<el-descriptions-item label="社团类型">
<dict-tag :options="stgl_club_type" :value="viewForm.clubType" />
</el-descriptions-item>
<el-descriptions-item label="归属">{{ affiliationOptions.find(o => o.deptId === String(viewForm.affiliation))?.deptName || '暂未设置' }}</el-descriptions-item>
<el-descriptions-item label="成立日期">{{ viewForm.establishDate || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="成员规模上限">{{ viewForm.memberLimit || '不限' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ viewForm.email || '暂无' }}</el-descriptions-item>
@@ -468,6 +514,19 @@
</div>
</div>
<!-- 物资保管员信息 -->
<div class="position-group">
<div class="position-title">物资保管员:</div>
<div class="position-content">
<template v-if="viewForm.materialKeeperName">
<span class="info-item"><strong>姓名:</strong>{{ viewForm.materialKeeperName }}</span>
</template>
<template v-else>
<span class="no-data">暂无物资保管员信息</span>
</template>
</div>
</div>
<!-- 指导老师信息 -->
<div class="position-group">
<div class="position-title">指导老师:</div>
@@ -682,11 +741,14 @@ import {
import { StglClubVO, StglClubQuery, StglClubForm } from '@/api/stgl/club/types';
import StudentSelector from '@/components/StudentSelector/index.vue';
import AdvisorManager from '@/components/AdvisorManager/index.vue';
import VicePresidentManager from '@/components/VicePresidentManager/index.vue';
import MemberManagementDialog from './components/MemberManagementDialog.vue';
import AdvisorHistoryDialog from '@/components/AdvisorHistoryDialog/index.vue';
import { StudentVO } from '@/api/xg/student/types';
import { AdvisorVO } from '@/components/AdvisorManager/index.vue';
import { VicePresidentVO } from '@/components/VicePresidentManager/index.vue';
import { Document } from '@element-plus/icons-vue';
import { listCollege } from '@/api/xg/xsjcxx/collegemsg';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter();
@@ -699,6 +761,22 @@ const hasInstructorPermission = computed(() => {
return proxy?.$auth.hasRoleOr(['stgl_instructor', 'stgl_stgl_admin', 'superadmin']);
});
// 归属选项:二级学院 + 学生处
interface DeptOption { deptId: string; deptName: string; }
const affiliationOptions = ref<DeptOption[]>([]);
const loadAffiliationOptions = async () => {
try {
const res = await listCollege();
const colleges = (res as any)?.rows || (res as any)?.data || [];
const options: DeptOption[] = colleges.map((c: any) => ({ deptId: String(c.deptId), deptName: c.deptName }));
options.push({ deptId: '202512231307170009', deptName: '学生处' });
affiliationOptions.value = options;
} catch (e) {
console.error('加载归属选项失败:', e);
}
};
loadAffiliationOptions();
// 社长权限控制 - 检查用户是否为各社团的社长(无论是否为创建者)
// 用于控制转让按钮显示和社长相关操作权限
const presidentRoles = ref<Record<string | number, boolean>>({});
@@ -811,6 +889,12 @@ const viewForm = reactive<StglClubForm>({
presidentMajor: undefined,
presidentClass: undefined,
presidentPhone: undefined,
presidentUserId: undefined,
materialKeeperUserId: undefined,
materialKeeperName: undefined,
materialKeeperStudent: null,
materialKeeperStudentNo: undefined,
materialKeeperPhone: undefined,
vicePresidentName: undefined,
vicePresidentStudentNo: undefined,
advisorName: undefined,
@@ -843,11 +927,19 @@ const initFormData: StglClubForm = {
presidentMajor: undefined,
presidentClass: undefined,
presidentPhone: undefined,
presidentUserId: undefined,
materialKeeperUserId: undefined,
materialKeeperName: undefined,
materialKeeperStudent: null,
materialKeeperStudentNo: undefined,
materialKeeperPhone: undefined,
vicePresidents: [],
advisors: [],
advisorName: undefined,
advisorDept: undefined,
memberLimit: undefined,
affiliation: undefined,
departments: undefined,
recruitCycle: undefined,
recruitTime: undefined,
@@ -983,6 +1075,9 @@ const handleUpdate = async (row?: StglClubVO) => {
if (res.data.sighOpen !== undefined && res.data.sighOpen !== null) {
(res.data as any).sighOpen = String(res.data.sighOpen);
}
if (res.data.affiliation !== undefined && res.data.affiliation !== null) {
(res.data as any).affiliation = String(res.data.affiliation);
}
// 处理社长信息:将简单字段转换为复杂对象结构
if (res.data.presidentName) {
@@ -998,7 +1093,54 @@ const handleUpdate = async (row?: StglClubVO) => {
(res.data as any).presidentStudent = null;
}
// 处理物资保管员信息:如果有userId则构建选择器对象
if (res.data.materialKeeperUserId) {
(res.data as any).materialKeeperStudent = {
name: res.data.materialKeeperName,
phone: ''
};
} else {
(res.data as any).materialKeeperStudent = null;
}
// 处理副社长信息:优先从JSON字段读取,如果没有则使用单一字段
let vicePresidentsData: VicePresidentVO[] = [];
if (res.data.vicePresidentsJson) {
try {
const parsed = JSON.parse(res.data.vicePresidentsJson);
vicePresidentsData = parsed.map((vp: any) => ({
student: {
id: vp.studentId || vp.studentNo || '',
name: vp.name || '',
studentNo: vp.studentNo || '',
phone: vp.phone || '',
collegeName: vp.dept || '',
majorName: vp.major || '',
className: vp.class || ''
} as StudentVO,
name: vp.name || '',
dept: vp.dept || '',
major: vp.major || '',
class: vp.class || '',
phone: vp.phone || ''
}));
} catch (error) {
console.warn('解析副社长JSON数据失败:', error);
vicePresidentsData = [];
}
} else if (res.data.vicePresidentName) {
vicePresidentsData = [{
student: {
name: res.data.vicePresidentName,
studentNo: res.data.vicePresidentStudentNo || ''
} as StudentVO,
name: res.data.vicePresidentName,
dept: res.data.vicePresidentDept || '',
major: res.data.vicePresidentMajor || '',
class: res.data.vicePresidentClass || '',
phone: res.data.vicePresidentPhone || ''
}];
}
// 处理指导老师信息:优先从JSON字段读取,如果没有则使用单一字段
let advisorsData = [];
@@ -1036,6 +1178,7 @@ const handleUpdate = async (row?: StglClubVO) => {
// 然后单独设置指导老师数据,避免触发 watch 时的冲突
nextTick(() => {
form.value.advisors = advisorsData;
form.value.vicePresidents = vicePresidentsData;
});
dialog.visible = true;
@@ -1071,6 +1214,26 @@ const submitForm = () => {
// 🔥 新增:同时保存JSON字段和兼容性字段
submitData.advisorsJson = JSON.stringify(submitData.advisors || []);
// 处理副社长数据:序列化为JSON
if (submitData.vicePresidents && Array.isArray(submitData.vicePresidents)) {
submitData.vicePresidentsJson = JSON.stringify(submitData.vicePresidents);
// 兼容旧字段:取第一个副社长
if (submitData.vicePresidents.length > 0) {
const firstVp = submitData.vicePresidents[0];
submitData.vicePresidentName = firstVp.name;
submitData.vicePresidentStudentNo = firstVp.student?.studentNo || '';
} else {
submitData.vicePresidentName = '';
submitData.vicePresidentStudentNo = '';
}
}
// 清理前端专用字段,不发送到后端
delete submitData.materialKeeperStudent;
delete submitData.materialKeeperStudentNo;
delete submitData.materialKeeperPhone;
delete submitData.presidentStudent;
if (form.value.id) {
await updateStglClub(submitData).finally(() => (buttonLoading.value = false));
} else {
@@ -1255,6 +1418,7 @@ const handlePresidentChange = (student: StudentVO | null) => {
form.value.presidentMajor = student.majorName;
form.value.presidentClass = student.className;
form.value.presidentPhone = student.phone;
form.value.presidentUserId = student.id;
} else {
form.value.presidentName = undefined;
form.value.presidentStudentNo = undefined;
@@ -1262,14 +1426,29 @@ const handlePresidentChange = (student: StudentVO | null) => {
form.value.presidentMajor = undefined;
form.value.presidentClass = undefined;
form.value.presidentPhone = undefined;
form.value.presidentUserId = undefined;
}
// 手动触发相关字段的验证,确保表单验证状态正确更新
nextTick(() => {
clubFormRef.value?.validateField(['presidentName', 'presidentStudentNo']);
});
};
/** 物资保管员选择变化处理 */
const handleMaterialKeeperChange = (student: StudentVO | null) => {
if (student) {
form.value.materialKeeperName = student.name;
form.value.materialKeeperStudentNo = student.studentNo;
form.value.materialKeeperPhone = student.phone;
form.value.materialKeeperUserId = student.id;
} else {
form.value.materialKeeperName = undefined;
form.value.materialKeeperStudentNo = undefined;
form.value.materialKeeperPhone = undefined;
form.value.materialKeeperUserId = undefined;
}
};
// 防止递归更新的锁
const syncLock = ref(false);
+1 -1
View File
@@ -126,7 +126,7 @@
v-hasPermi="['stgl:clubmember:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="审核" placement="top" v-if="scope.row.status === 0">
<el-tooltip content="审核" placement="top" v-if="scope.row.status === 0 || scope.row.status === 1">
<el-button
link
type="success"
@@ -0,0 +1,275 @@
<template>
<div class="p-2 page-container">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="班级" prop="classId">
<el-select v-model="queryParams.classId" placeholder="请选择班级" clearable style="width: 100%" @change="handleClassChange">
<el-option v-for="item in classList" :key="item.id" :label="item.className" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="RefreshRight" @click="handleQuery">刷新</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 统计信息 -->
<div v-if="queryParams.classId" class="statistics-bar">
<el-tag type="success" size="large" effect="plain">
当前共有 <span class="statistics-count">{{ total }}</span> 名学生正在参加活动
</el-tag>
</div>
<!-- 未选择班级提示 -->
<div v-if="!queryParams.classId" class="no-data-container">
<el-empty description="请先选择班级" />
</div>
<!-- 活动列表表格 -->
<el-table v-else v-loading="loading" border :data="activityList" row-key="id">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="学生姓名" align="center" prop="studentName" min-width="40" />
<el-table-column label="学号" align="center" prop="studentNo" min-width="45" />
<el-table-column label="性别" align="center" prop="gender" width="70">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender" />
</template>
</el-table-column>
<el-table-column label="活动名称" align="center" min-width="60">
<template #default="scope">
<span v-if="scope.row.activityName" class="activity-name">{{ scope.row.activityName }}</span>
<span v-else class="no-data">-</span>
</template>
</el-table-column>
<el-table-column label="所属社团/队伍" align="center" prop="organizationName" min-width="50">
<template #default="scope">
<span v-if="scope.row.organizationName">{{ scope.row.organizationName }}</span>
<span v-else class="no-data">-</span>
</template>
</el-table-column>
<el-table-column label="活动开始时间" align="center" width="160">
<template #default="scope">
<span v-if="scope.row.startTime">{{ formatTime(scope.row.startTime) }}</span>
<span v-else class="no-data">-</span>
</template>
</el-table-column>
<el-table-column label="活动结束时间" align="center" width="160">
<template #default="scope">
<span v-if="scope.row.endTime">{{ formatTime(scope.row.endTime) }}</span>
<span v-else class="no-data">-</span>
</template>
</el-table-column>
<el-table-column label="活动地址" align="center" prop="location" min-width="50">
<template #default="scope">
<span v-if="scope.row.location">{{ scope.row.location }}</span>
<span v-else class="no-data">-</span>
</template>
</el-table-column>
</el-table>
<div v-if="queryParams.classId" class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
</div>
</template>
<script setup name="StglClassCurrentActivity" lang="ts">
import { getTeacherClasses, getCurrentActivities } from '@/api/stgl/stclass';
import { StglClassQuery, CurrentActivityVO, StglClassDetailVO } from '@/api/stgl/stclass/types';
import dayjs from 'dayjs';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_user_sex } = toRefs<any>(proxy?.useDict('sys_user_sex'));
const loading = ref(false);
const showSearch = ref(true);
const total = ref(0);
const activityList = ref<CurrentActivityVO[]>([]);
const classList = ref<StglClassDetailVO[]>([]);
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<StglClassQuery>({
pageNum: 1,
pageSize: 10,
classId: undefined,
studentName: undefined,
studentNo: undefined
});
/** 格式化时间 yyyy-MM-dd HH:mm */
const formatTime = (time: string) => {
return dayjs(time).format('YYYY-MM-DD HH:mm');
};
/** 查询班主任的班级列表 */
const getTeacherClassList = async () => {
try {
const res = await getTeacherClasses();
if ((res.code === 200 || res.code === 0) && res.data) {
classList.value = res.data;
if (classList.value.length > 0) {
queryParams.value.classId = classList.value[0].id;
await getList();
}
}
} catch (error) {
console.error('获取班级列表失败:', error);
proxy?.$modal.msgError('获取班级列表失败');
}
};
/** 查询当前活动列表 */
const getList = async () => {
if (!queryParams.value.classId) return;
loading.value = true;
try {
const res = await getCurrentActivities(queryParams.value);
if (res.code === 200 || res.code === 0) {
activityList.value = res.rows;
total.value = res.total;
}
} catch (error) {
console.error('查询当前活动列表失败:', error);
proxy?.$modal.msgError('查询当前活动列表失败');
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 班级切换处理 */
const handleClassChange = () => {
queryParams.value.pageNum = 1;
getList();
};
onMounted(() => {
getTeacherClassList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
background-color: #f5f5f5;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
/* 统计信息栏 */
.statistics-bar {
padding: 12px 0 16px 0;
display: flex;
align-items: center;
}
.statistics-count {
font-size: 18px;
font-weight: bold;
color: #409eff;
}
/* 活动名称样式 */
.activity-name {
color: #303133;
font-weight: 500;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 无数据样式 */
.no-data {
color: #909399;
font-size: 14px;
}
.no-data-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
</style>