This commit is contained in:
那些花儿
2025-05-13 18:27:15 +08:00
parent 178f9b39ff
commit 7949eec70d
31 changed files with 3052 additions and 337 deletions
@@ -11,8 +11,10 @@ import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.Daos;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -165,5 +167,18 @@ public class ClientActivityController {
return Result.success(actUser);
}
/**
* 用户协议
*/
@At("/agreement/?")
public Result agreement(Long activityId) {
Dao extDao = Daos.ext(dao, FieldFilter.create(Activity.class, "protocol"));
Activity activity = extDao.fetch(Activity.class, activityId);
if (activity == null) {
return Result.success();
}
return Result.success().addData(activity.getProtocol());
}
}
@@ -7,9 +7,11 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.module.activity.models.ActGroup;
import com.budwk.app.module.activity.models.ActGroupUser;
import com.budwk.app.module.activity.models.ActItemGroupScore;
import com.budwk.app.module.activity.models.ActivityItem;
import com.budwk.app.module.activity.service.ActGroupService;
import com.budwk.app.module.activity.service.ActUserService;
import com.budwk.app.module.activity.vo.ActGroupVo;
import com.budwk.app.module.activity.vo.ActManualGroupVo;
import com.budwk.app.module.activity.vo.ActUnGroupUserVo;
import com.budwk.app.web.commons.slog.annotation.SLog;
import lombok.extern.slf4j.Slf4j;
@@ -29,6 +31,7 @@ import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
/**
* @version 1.0
@@ -59,7 +62,7 @@ public class ActGroupController {
@At
@SaCheckPermission("activity.group")
public Result pageData(PageForm pageForm, String activityId, String groupName){
public Result pageData(PageForm pageForm, String activityId, String groupName) {
Sql sql = Sqls.create("""
SELECT
ag.*,
@@ -95,12 +98,13 @@ public class ActGroupController {
/**
* 查询签到人员
*
* @param activityId
* @return
*/
@At
@SaCheckPermission("activity.group")
public Result querySingUpData(String activityId){
public Result querySingUpData(String activityId) {
Sql sql = Sqls.create("""
SELECT
act.id AS activityId,
@@ -136,7 +140,7 @@ public class ActGroupController {
@At
@SaCheckPermission("activity.group")
public Result queryGroupList(String activityId){
public Result queryGroupList(String activityId) {
List<ActGroup> query = dao.query(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", activityId));
return Result.success().addData(query);
}
@@ -144,7 +148,7 @@ public class ActGroupController {
@At
@SaCheckPermission("activity.group")
public Result queryGroupUser(String activityId, String groupId){
public Result queryGroupUser(String activityId, String groupId) {
Sql sql = Sqls.create("""
SELECT
u.*,
@@ -167,7 +171,7 @@ public class ActGroupController {
@At
@SaCheckPermission("activity.group")
@SLog(tag = "活动分组", msg = "随机分组")
public Result groupSetting(ActGroupVo actGroupVo){
public Result groupSetting(ActGroupVo actGroupVo) {
// 查询所有签到的用户,按照分组参数进行分组
actGroupService.groupSetting(actGroupVo);
return Result.success();
@@ -176,8 +180,8 @@ public class ActGroupController {
@At
@SaCheckPermission("activity.group")
public Result queryUnGroupUser(String activityId){
Sql sql = Sqls.create("""
public Result queryUnGroupUser(String activityId) {
Sql sql = Sqls.create("""
SELECT
u.*
FROM
@@ -193,8 +197,70 @@ public class ActGroupController {
@At
@SaCheckPermission("activity.group")
@SLog(tag = "活动分组", msg = "设置未分组人员")
public Result unGroupUserSetting(ActUnGroupUserVo actUnGroupUserVo){
public Result unGroupUserSetting(ActUnGroupUserVo actUnGroupUserVo) {
actGroupService.unGroupUserSetting(actUnGroupUserVo);
return Result.success();
}
@At
@SaCheckPermission("activity.group")
@SLog(tag = "活动分组", msg = "查询未分组人员")
public Result unGroupUsers(Long activityId) {
List<NutMap> list = actGroupService.unGroupUsers(activityId);
return Result.success(list);
}
@At
@SaCheckPermission("activity.group")
@SLog(tag = "活动分组", msg = "查询分组信息及组员")
public Result listGroup(Long activityId) {
List<NutMap> list = actGroupService.listGroupAndUsers(activityId);
return Result.success(list);
}
@At
@SaCheckPermission("activity.group")
@SLog(tag = "活动分组", msg = "手动设置分组")
public Result manualSetGroup(@Param("actGroups") ActGroup[] actGroups, @Param("activityId") Long activityId) {
dao.clear(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", activityId));
dao.clear(ActGroupUser.class, Cnd.where(ActGroupUser::getActivityId, "=", activityId));
for (int i = 0; i < actGroups.length; i++) {
actGroups[i].setActivityId(activityId);
actGroups[i].setSort(i + 1);
dao.insert(actGroups[i]);
for (ActGroupUser user : actGroups[i].getGroupUsers()) {
user.setActivityId(activityId);
user.setGroupId(actGroups[i].getId());
if (user.getUserId() == null) {
return Result.error("用户ID不能为空");
}
}
dao.insert(actGroups[i].getGroupUsers());
}
// 查询项目列表
List<ActivityItem> items = dao.query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", activityId));
// 清空分组积分
dao.clear(ActItemGroupScore.class, Cnd.where(ActItemGroupScore::getActivityId, "=", activityId));
// 往项目得分表里面插记录
List<ActItemGroupScore> itemGroupScoreList = new ArrayList<>();
for (ActGroup group : actGroups) {
// 构建小组项目得分表
List<ActItemGroupScore> groupScoreList = items.stream().map(item -> {
ActItemGroupScore itemGroupScore = new ActItemGroupScore();
itemGroupScore.setActivityId(activityId);
itemGroupScore.setItemId(item.getId());
itemGroupScore.setGroupId(group.getId());
return itemGroupScore;
}).toList();
itemGroupScoreList.addAll(groupScoreList);
}
dao.insert(itemGroupScoreList);
return Result.success();
}
}
@@ -0,0 +1,89 @@
package com.budwk.app.module.activity.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.PageForm;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.module.activity.models.ActRankScore;
import com.budwk.app.module.activity.service.ActRankScoreService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Arrays;
import java.util.List;
@IocBean
@At("/platform/activity/rankScore")
@Slf4j
@Ok("json:full")
@Api(tags = "活动名次积分对应")
public class ActRankScoreController {
@Inject
private ActRankScoreService actRankScoreService;
@At("")
@SaCheckPermission("activity.rankScore")
@Ok("beetl:/platform/activity/rankScore/index.html")
public void index() {
}
@At
@SaCheckPermission("activity.rankScore")
@ApiOperation(value = "分页查询", nickname = "/pageData", httpMethod = "GET")
public Result pageData(PageForm pageForm) {
Cnd cnd = Cnd.NEW();
cnd.desc(ActRankScore::getScore);
Pagination pagination = actRankScoreService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
@At
@SaCheckPermission("activity.rankScore")
@ApiOperation(value = "保存", nickname = "/save", httpMethod = "POST")
public Result save(@Param("actRankScore") ActRankScore[] actRankScore) {
// 名次不能重复
long count = Arrays.stream(actRankScore).map(ActRankScore::getRanking).distinct().count();
if (count != actRankScore.length) {
return Result.error("名次不能重复");
}
List<String> rankingList = Arrays.stream(actRankScore).map(ActRankScore::getRanking).toList();
if (actRankScoreService.count(Cnd.where(ActRankScore::getRanking, "in", rankingList)) > 0) {
return Result.error("名次已存在");
}
actRankScoreService.insertOrUpdate(actRankScore);
return Result.success();
}
@At
@SaCheckPermission("activity.rankScore")
@ApiOperation(value = "更新", nickname = "/update", httpMethod = "POST")
public Result update(ActRankScore actRankScore){
// 名次不能重复
long count = actRankScoreService.count(Cnd.where(ActRankScore::getRanking, "=", actRankScore.getRanking())
.and(ActRankScore::getId, "!=", actRankScore.getId()));
if (count > 0) {
return Result.error("名次已存在");
}
actRankScoreService.updateIgnoreNull(actRankScore);
return Result.success();
}
@At
@ApiOperation(value = "删除", nickname = "/delete", httpMethod = "POST")
public Result delete(Long id) {
actRankScoreService.delete(id);
return Result.success();
}
}
@@ -0,0 +1,111 @@
package com.budwk.app.module.activity.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.module.activity.models.ActGroup;
import com.budwk.app.module.activity.models.ActItemGroupScore;
import com.budwk.app.module.activity.models.ActRankScore;
import com.budwk.app.module.activity.models.ActivityItem;
import com.budwk.app.module.activity.service.ActivityManageService;
import com.budwk.app.web.commons.slog.annotation.SLog;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.math.BigDecimal;
import java.util.List;
@IocBean
@At("/platform/activity/scoreEntry")
@Ok("json:full")
@Api(tags = "成绩录入", value = "/platform/activity/scoreEntry")
public class ActScoreEntryController {
@Inject
private Dao dao;
@Inject
private ActivityManageService activityService;
@At("")
@SaCheckPermission("activity.scoreEntry")
@Ok("beetl:/platform/activity/scoreEntry/index.html")
public void index() {
}
@At("/list-activity")
@SaCheckPermission("activity.scoreEntry")
@ApiOperation(value = "获取活动列表", nickname = "/list-activity", httpMethod = "GET")
public Result listAct(Integer year) {
Sql sql = Sqls.create("select id,title from activity $condition");
Cnd cnd = Cnd.NEW();
cnd.andEX("YEAR(startTime)", "=", year);
sql.setCondition(cnd);
return Result.success(activityService.listMap(sql));
}
/**
* 获取队伍
*/
@At("/list-group/?")
@SaCheckPermission("activity.scoreEntry")
@ApiOperation(value = "获取队伍", nickname = "/list-group", httpMethod = "GET")
public Result listGroup(Long activityId) {
List<ActGroup> groupList = activityService.dao().query(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", activityId));
return Result.success(groupList);
}
/**
* 获取项目列表
*/
@At("/list-item/?")
@SaCheckPermission("activity.scoreEntry")
@ApiOperation(value = "获取项目列表", nickname = "/list-item", httpMethod = "GET")
public Result listItem(Long activityId) {
List<ActivityItem> items = dao.query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", activityId).asc(ActivityItem::getSort));
return Result.success(items);
}
@At("/list-item-score/?")
@SaCheckPermission("activity.scoreEntry")
@ApiOperation(value = "获取队伍项目名次得分", nickname = "/list-item-score", httpMethod = "GET")
public Result listItemScore(Long activityId) {
List<ActItemGroupScore> scoreList = dao.query(ActItemGroupScore.class, Cnd.where(ActItemGroupScore::getActivityId, "=", activityId));
return Result.success(scoreList);
}
@At("/list-rank-score")
@SaCheckPermission("activity.scoreEntry")
@ApiOperation(value = "获取名次积分对应", nickname = "/list-rank-score", httpMethod = "GET")
public Result listRankScore() {
List<ActRankScore> rankScores = dao.query(ActRankScore.class, Cnd.NEW().desc(ActRankScore::getScore));
return Result.success(rankScores);
}
@At("/save-score")
@SaCheckPermission("activity.group")
@ApiOperation(value = "保存成绩", nickname = "/save-score", httpMethod = "POST")
public Result saveScore(@Param("scores") ActItemGroupScore[] actItemGroupScore) {
// 更新分数和名次
dao.updateIgnoreNull(actItemGroupScore);
// 更新总得分
// ActGroup actGroup = dao.fetch(ActGroup.class, actItemGroupScore.getGroupId());
// BigDecimal newTotalScore = actGroup.getTotalScore().add(actItemGroupScore.getScore());
// actGroup.setTotalScore(newTotalScore);
// dao.update(actGroup);
return Result.success();
}
}
@@ -71,7 +71,7 @@ public class ActUserController {
@At
@SaCheckPermission("activity.user")
public Result pageData(ActUserPageForm pageForm){
public Result pageData(ActUserPageForm pageForm) {
Sql sql = actUserService.getSignUpSql(pageForm);
Pagination pagination = actUserService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -83,7 +83,7 @@ public class ActUserController {
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("activity.user")
@SLog(tag = "报名人员管理", msg = "电脑端替他人报名")
public Result doAddActUser(ClientActivitySignVO vo){
public Result doAddActUser(ClientActivitySignVO vo) {
try {
lock.lock();
// 报名时间
@@ -158,7 +158,8 @@ public class ActUserController {
@At
@SaCheckPermission("activity.user")
@SLog(tag = "报名人员管理", msg = "删除报名人员")
public Result doDeleteActUser(Long id){
@Aop(TransAop.READ_COMMITTED)
public Result doDeleteActUser(Long id) {
dao.delete(ActUser.class, id);
return Result.success();
}
@@ -167,7 +168,7 @@ public class ActUserController {
@At
@SaCheckPermission("activity.user")
@SLog(tag = "报名人员管理", msg = "修改报名人员信息")
public Result doModifyActUser(ActUser actUser){
public Result doModifyActUser(ActUser actUser) {
dao.updateIgnoreNull(actUser);
return Result.success();
}
@@ -175,14 +176,23 @@ public class ActUserController {
@At
@SaCheckPermission("activity.user")
@SLog(tag = "报名人员管理", msg = "设置已签到")
public Result proxySignIn(Long id){
dao.update(ActUser.class, Chain.make("isSignIn",1).add("signInTime",LocalDateTime.now()),Cnd.where(ActUser::getId,"=",id));
public Result proxySignIn(Long id) {
dao.update(ActUser.class, Chain.make("isSignIn", 1).add("signInTime", LocalDateTime.now()), Cnd.where(ActUser::getId, "=", id));
return Result.success();
}
@At
@SaCheckPermission("activity.user")
public Result findSignUserInfo(Long id){
@SLog(tag = "报名人员管理", msg = "取消签到")
public Result cancelSignIn(Long id) {
dao.update(ActUser.class, Chain.make("isSignIn", 0).add("signInTime", null), Cnd.where(ActUser::getId, "=", id));
return Result.success();
}
@At
@SaCheckPermission("activity.user")
public Result findSignUserInfo(Long id) {
return Result.success(dao.fetch(ActUser.class, Cnd.where(ActUser::getId, "=", id)));
}
@@ -192,7 +202,7 @@ public class ActUserController {
*/
@At
@SaCheckPermission("activity.user")
public Result getActivityList(String year){
public Result getActivityList(String year) {
List<Activity> activityList = dao.query(Activity.class, Cnd.NEW().andEX("YEAR(startTime)", "=", year).and(Activity::getDelFlag, "=", false));
return Result.success(activityList);
}
@@ -200,7 +210,7 @@ public class ActUserController {
@At
@SaCheckPermission("activity.user")
public Result queryUser(String query, String activityId){
public Result queryUser(String query, String activityId) {
Sql sql = Sqls.create("SELECT id,username,phone FROM client_user $condition");
Cnd cnd = Cnd.NEW();
SqlExpressionGroup seg = new SqlExpressionGroup();
@@ -239,7 +249,7 @@ public class ActUserController {
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, nutMaps);
workbook.write(response.getOutputStream());
}catch (Exception e){
} catch (Exception e) {
e.printStackTrace();
}
@@ -0,0 +1,30 @@
package com.budwk.app.module.activity.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import java.math.BigDecimal;
@Data
@Table("act_rank_score")
@Comment("项目名次积分对应表")
@EqualsAndHashCode(callSuper = true)
@TableMeta("{'mysql-charset':'utf8mb4'}")
public class ActRankScore extends BaseModel {
@Id
@Comment("ID")
private Long id;
@Column
@Comment("名次")
private String ranking;
@Column
@Comment("积分")
@ColDefine(customType = "decimal", width = 10, precision = 2)
private BigDecimal score;
}
@@ -122,4 +122,27 @@ public class ActUser extends BaseModel {
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean isGrant;
@Column
@Comment("是否展示才艺")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean isShowSkill;
@Column
@Comment("才艺名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String skillName;
@Column
@Comment("所需器材")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String equip;
@Column
@Comment("是否参加嘉年华")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean isParty;
}
@@ -4,6 +4,9 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.module.activity.models.ActGroup;
import com.budwk.app.module.activity.vo.ActGroupVo;
import com.budwk.app.module.activity.vo.ActUnGroupUserVo;
import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* @ClassName ActGroupService
@@ -26,4 +29,16 @@ public interface ActGroupService extends BaseService<ActGroup> {
* @param unGroupUserVo
*/
void unGroupUserSetting(ActUnGroupUserVo unGroupUserVo);
/**
* 查询未分组人员
* @param activityId
*/
List<NutMap> unGroupUsers(Long activityId);
/**
* 查询分组信息及组员
* @param activityId
*/
List<NutMap> listGroupAndUsers(Long activityId);
}
@@ -0,0 +1,7 @@
package com.budwk.app.module.activity.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.module.activity.models.ActRankScore;
public interface ActRankScoreService extends BaseService<ActRankScore> {
}
@@ -1,5 +1,6 @@
package com.budwk.app.module.activity.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.module.activity.models.*;
import com.budwk.app.module.activity.service.ActGroupService;
@@ -12,6 +13,7 @@ import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.*;
import java.util.stream.Collectors;
@@ -27,187 +29,227 @@ import java.util.stream.IntStream;
@IocBean(args = {"refer:dao"})
public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements ActGroupService {
public ActGroupServiceImpl(Dao dao) {
super(dao);
}
public ActGroupServiceImpl(Dao dao) {
super(dao);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void groupSetting(ActGroupVo groupVo) {
// 删除已经存在的分组信息
dao().clear(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", groupVo.getActivityId()));
dao().clear(ActGroupUser.class, Cnd.where(ActGroupUser::getActivityId, "=", groupVo.getActivityId()));
dao().clear(ActUnGroupUser.class, Cnd.where(ActUnGroupUser::getActivityId, "=", groupVo.getActivityId()));
dao().clear(ActItemGroupScore.class, Cnd.where(ActItemGroupScore::getActivityId, "=", groupVo.getActivityId()));
@Override
@Aop(TransAop.READ_COMMITTED)
public void groupSetting(ActGroupVo groupVo) {
// 删除已经存在的分组信息
dao().clear(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", groupVo.getActivityId()));
dao().clear(ActGroupUser.class, Cnd.where(ActGroupUser::getActivityId, "=", groupVo.getActivityId()));
dao().clear(ActUnGroupUser.class, Cnd.where(ActUnGroupUser::getActivityId, "=", groupVo.getActivityId()));
dao().clear(ActItemGroupScore.class, Cnd.where(ActItemGroupScore::getActivityId, "=", groupVo.getActivityId()));
// 查询报名了并且已经完成签到的人员
List<ActUser> signUpUserList = dao().query(ActUser.class, Cnd.where(ActUser::getActivityId, "=", groupVo.getActivityId())
.and(ActUser::getIsSignIn, "=", 1));
// 查询报名了并且已经完成签到的人员
List<ActUser> signUpUserList = dao().query(ActUser.class, Cnd.where(ActUser::getActivityId, "=", groupVo.getActivityId())
.and(ActUser::getIsSignIn, "=", 1));
int size = signUpUserList.size();
Integer oneGroupLimitUserNum = groupVo.getOneGroupLimitUserNum();
int size = signUpUserList.size();
Integer oneGroupLimitUserNum = groupVo.getOneGroupLimitUserNum();
int result = Math.floorDiv(size, oneGroupLimitUserNum);
// 初始化分组
List<List<ActUser>> groups = new ArrayList<>();
for (int i = 0; i < result; i++) {
groups.add(new ArrayList<>());
}
int result = Math.floorDiv(size, oneGroupLimitUserNum);
// 初始化分组
List<List<ActUser>> groups = new ArrayList<>();
for (int i = 0; i < result; i++) {
groups.add(new ArrayList<>());
}
// 分离男女用户
Map<String, List<ActUser>> genderMap = signUpUserList.stream()
.collect(Collectors.groupingBy(user -> "男".equals(user.getSex()) ? "男" : "女"));
// 分离男女用户
Map<String, List<ActUser>> genderMap = signUpUserList.stream()
.collect(Collectors.groupingBy(user -> "男".equals(user.getSex()) ? "男" : "女"));
List<ActUser> males = genderMap.getOrDefault("男", Collections.emptyList());
List<ActUser> females = genderMap.getOrDefault("女", Collections.emptyList());
List<ActUser> males = genderMap.getOrDefault("男", Collections.emptyList());
List<ActUser> females = genderMap.getOrDefault("女", Collections.emptyList());
// 随机化列表
Collections.shuffle(males);
Collections.shuffle(females);
// 随机化列表
Collections.shuffle(males);
Collections.shuffle(females);
// 尝试分配
List<ActUser> unassignedUsers = new ArrayList<>();
// 尝试分配
List<ActUser> unassignedUsers = new ArrayList<>();
int maleIndex = 0, femaleIndex = 0;
while (maleIndex < males.size() || femaleIndex < females.size()) {
boolean assigned = false;
for (List<ActUser> group : groups) {
if (group.size() < groupVo.getOneGroupLimitUserNum()) {
int currentMaleCount = (int) group.stream().filter(u -> "男".equals(u.getSex())).count();
int currentFemaleCount = (int) group.stream().filter(u -> "女".equals(u.getSex())).count();
int maleIndex = 0, femaleIndex = 0;
while (maleIndex < males.size() || femaleIndex < females.size()) {
boolean assigned = false;
for (List<ActUser> group : groups) {
if (group.size() < groupVo.getOneGroupLimitUserNum()) {
int currentMaleCount = (int) group.stream().filter(u -> "男".equals(u.getSex())).count();
int currentFemaleCount = (int) group.stream().filter(u -> "女".equals(u.getSex())).count();
if (maleIndex < males.size() && (groupVo.getManUserNum() == -1 || currentMaleCount < groupVo.getManUserNum())) {
group.add(males.get(maleIndex++));
assigned = true;
} else if (femaleIndex < females.size() && (groupVo.getWomanUserNum() == -1 || currentFemaleCount < groupVo.getWomanUserNum())) {
group.add(females.get(femaleIndex++));
assigned = true;
}
}
}
if (!assigned) {
break;
}
}
if (maleIndex < males.size() && (groupVo.getManUserNum() == -1 || currentMaleCount < groupVo.getManUserNum())) {
group.add(males.get(maleIndex++));
assigned = true;
} else if (femaleIndex < females.size() && (groupVo.getWomanUserNum() == -1 || currentFemaleCount < groupVo.getWomanUserNum())) {
group.add(females.get(femaleIndex++));
assigned = true;
}
}
}
if (!assigned) {
break;
}
}
unassignedUsers.addAll(males.subList(maleIndex, males.size()));
unassignedUsers.addAll(females.subList(femaleIndex, females.size()));
unassignedUsers.addAll(males.subList(maleIndex, males.size()));
unassignedUsers.addAll(females.subList(femaleIndex, females.size()));
// 往组别表里面添加记录
List<ActGroup> groupList = IntStream.range(0, groups.size())
.mapToObj(index -> {
ActGroup actGroup = new ActGroup();
actGroup.setActivityId(groupVo.getActivityId());
actGroup.setGroupName("第" + (index + 1) + "队");
actGroup.setSort(index + 1);
// 往组别表里面添加记录
List<ActGroup> groupList = IntStream.range(0, groups.size())
.mapToObj(index -> {
ActGroup actGroup = new ActGroup();
actGroup.setActivityId(groupVo.getActivityId());
actGroup.setGroupName("第" + (index + 1) + "队");
actGroup.setSort(index + 1);
List<ActGroupUser> userList = groups.get(index).stream().map(v -> {
ActGroupUser user = new ActGroupUser();
user.setUserId(v.getUserId());
user.setActivityId(groupVo.getActivityId());
return user;
}).collect(Collectors.toList());
List<ActGroupUser> userList = groups.get(index).stream().map(v -> {
ActGroupUser user = new ActGroupUser();
user.setUserId(v.getUserId());
user.setActivityId(groupVo.getActivityId());
return user;
}).collect(Collectors.toList());
actGroup.setGroupUsers(userList);
return actGroup;
}).toList();
actGroup.setGroupUsers(userList);
return actGroup;
}).toList();
// 所有项目
List<ActivityItem> actItemList = dao().query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", groupVo.getActivityId()));
// 往项目得分表里面插记录
List<ActItemGroupScore> itemGroupScoreList = new ArrayList<>();
// 所有项目
List<ActivityItem> actItemList = dao().query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", groupVo.getActivityId()));
// 往项目得分表里面插记录
List<ActItemGroupScore> itemGroupScoreList = new ArrayList<>();
for (ActGroup group : groupList) {
dao().insertWith(group, "groupUsers");
for (ActGroup group : groupList) {
dao().insertWith(group, "groupUsers");
// 构建小组项目得分表
List<ActItemGroupScore> groupScoreList = actItemList.stream().map(item -> {
ActItemGroupScore itemGroupScore = new ActItemGroupScore();
itemGroupScore.setActivityId(groupVo.getActivityId());
itemGroupScore.setItemId(item.getId());
itemGroupScore.setGroupId(group.getId());
return itemGroupScore;
}).toList();
itemGroupScoreList.addAll(groupScoreList);
}
dao().insert(itemGroupScoreList);
// 构建小组项目得分表
List<ActItemGroupScore> groupScoreList = actItemList.stream().map(item -> {
ActItemGroupScore itemGroupScore = new ActItemGroupScore();
itemGroupScore.setActivityId(groupVo.getActivityId());
itemGroupScore.setItemId(item.getId());
itemGroupScore.setGroupId(group.getId());
return itemGroupScore;
}).toList();
itemGroupScoreList.addAll(groupScoreList);
}
dao().insert(itemGroupScoreList);
// 添加未能正确分组的人员
List<ActUnGroupUser> userList = unassignedUsers.stream().map(v -> {
ActUnGroupUser user = new ActUnGroupUser();
user.setUserId(v.getUserId());
user.setActivityId(groupVo.getActivityId());
return user;
}).toList();
dao().insert(userList);
}
// 添加未能正确分组的人员
List<ActUnGroupUser> userList = unassignedUsers.stream().map(v -> {
ActUnGroupUser user = new ActUnGroupUser();
user.setUserId(v.getUserId());
user.setActivityId(groupVo.getActivityId());
return user;
}).toList();
dao().insert(userList);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void unGroupUserSetting(ActUnGroupUserVo unGroupUserVo) {
// 查询还没有分组的人员
List<ActUnGroupUser> query = dao().query(ActUnGroupUser.class, Cnd.where(ActUnGroupUser::getActivityId, "=", unGroupUserVo.getActivityId()));
Set<Long> userIds = query.stream().map(ActUnGroupUser::getUserId).collect(Collectors.toSet());
// 删除这些人员的分组信息
dao().clear(ActGroupUser.class, Cnd.where(ActGroupUser::getActivityId, "=", unGroupUserVo.getActivityId()).and(ActGroupUser::getUserId, "in", userIds));
@Override
@Aop(TransAop.READ_COMMITTED)
public void unGroupUserSetting(ActUnGroupUserVo unGroupUserVo) {
// 查询还没有分组的人员
List<ActUnGroupUser> query = dao().query(ActUnGroupUser.class, Cnd.where(ActUnGroupUser::getActivityId, "=", unGroupUserVo.getActivityId()));
Set<Long> userIds = query.stream().map(ActUnGroupUser::getUserId).collect(Collectors.toSet());
// 删除这些人员的分组信息
dao().clear(ActGroupUser.class, Cnd.where(ActGroupUser::getActivityId, "=", unGroupUserVo.getActivityId()).and(ActGroupUser::getUserId, "in", userIds));
if (unGroupUserVo.getIsSeparateGroup() != null && unGroupUserVo.getIsSeparateGroup()) {
// 构建分组用户
List<ActGroupUser> userList = query.stream().map(v -> {
ActGroupUser user = new ActGroupUser();
user.setActivityId(unGroupUserVo.getActivityId());
user.setUserId(v.getUserId());
if (v.getUserId().equals(unGroupUserVo.getLeaderId())) {
user.setIsLeader(true);
}
return user;
}).toList();
if (unGroupUserVo.getIsSeparateGroup() != null && unGroupUserVo.getIsSeparateGroup()) {
// 构建分组用户
List<ActGroupUser> userList = query.stream().map(v -> {
ActGroupUser user = new ActGroupUser();
user.setActivityId(unGroupUserVo.getActivityId());
user.setUserId(v.getUserId());
if (v.getUserId().equals(unGroupUserVo.getLeaderId())) {
user.setIsLeader(true);
}
return user;
}).toList();
// 查询出这个活动下最大的排序
ActGroup sort = dao().fetch(ActGroup.class, Cnd.NEW().desc("sort"));
// 查询出这个活动下最大的排序
ActGroup sort = dao().fetch(ActGroup.class, Cnd.NEW().desc("sort"));
// 单独分组构建
ActGroup actGroup = new ActGroup();
actGroup.setActivityId(unGroupUserVo.getActivityId());
actGroup.setGroupName(unGroupUserVo.getGroupName());
actGroup.setSort(sort.getSort() + 1);
actGroup.setGroupUsers(userList);
dao().insertWith(actGroup, "groupUsers");
// 单独分组构建
ActGroup actGroup = new ActGroup();
actGroup.setActivityId(unGroupUserVo.getActivityId());
actGroup.setGroupName(unGroupUserVo.getGroupName());
actGroup.setSort(sort.getSort() + 1);
actGroup.setGroupUsers(userList);
dao().insertWith(actGroup, "groupUsers");
// 同时还要往小组得分记录表中查活动项目的数据
List<ActivityItem> itemList = dao().query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", unGroupUserVo.getActivityId()));
// 构建小组项目得分表
List<ActItemGroupScore> scoreList = itemList.stream().map(v -> {
ActItemGroupScore itemGroupScore = new ActItemGroupScore();
itemGroupScore.setActivityId(unGroupUserVo.getActivityId());
itemGroupScore.setItemId(v.getId());
itemGroupScore.setGroupId(actGroup.getId());
return itemGroupScore;
}).toList();
dao().insert(scoreList);
} else {
// 不单独分组,分配到其他队伍
List<ActGroupUser> unGroupUser = unGroupUserVo.getUnGroupUser();
Map<Long, ActGroupUser> map = unGroupUser.stream().collect(Collectors.toMap(ActGroupUser::getUserId, v -> v));
// 同时还要往小组得分记录表中查活动项目的数据
List<ActivityItem> itemList = dao().query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", unGroupUserVo.getActivityId()));
// 构建小组项目得分表
List<ActItemGroupScore> scoreList = itemList.stream().map(v -> {
ActItemGroupScore itemGroupScore = new ActItemGroupScore();
itemGroupScore.setActivityId(unGroupUserVo.getActivityId());
itemGroupScore.setItemId(v.getId());
itemGroupScore.setGroupId(actGroup.getId());
return itemGroupScore;
}).toList();
dao().insert(scoreList);
} else {
// 不单独分组,分配到其他队伍
List<ActGroupUser> unGroupUser = unGroupUserVo.getUnGroupUser();
Map<Long, ActGroupUser> map = unGroupUser.stream().collect(Collectors.toMap(ActGroupUser::getUserId, v -> v));
List<ActGroupUser> userList = query.stream().map(v -> {
// 要保证传递过来的数据中,不在数据表中存储的这些人员不插入
ActGroupUser groupUser = map.get(v.getUserId());
if (groupUser != null) {
groupUser.setUserId(v.getUserId());
groupUser.setActivityId(unGroupUserVo.getActivityId());
groupUser.setGroupId(groupUser.getGroupId());
return groupUser;
} else {
return null;
}
}).filter(Objects::nonNull).toList();
dao().insert(userList);
}
// 分完之后,删除这个活动下的未分组人员
dao().clear(ActUnGroupUser.class, Cnd.where(ActUnGroupUser::getActivityId, "=", unGroupUserVo.getActivityId()));
}
List<ActGroupUser> userList = query.stream().map(v -> {
// 要保证传递过来的数据中,不在数据表中存储的这些人员不插入
ActGroupUser groupUser = map.get(v.getUserId());
if (groupUser != null) {
groupUser.setUserId(v.getUserId());
groupUser.setActivityId(unGroupUserVo.getActivityId());
groupUser.setGroupId(groupUser.getGroupId());
return groupUser;
} else {
return null;
}
}).filter(Objects::nonNull).toList();
dao().insert(userList);
}
// 分完之后,删除这个活动下的未分组人员
dao().clear(ActUnGroupUser.class, Cnd.where(ActUnGroupUser::getActivityId, "=", unGroupUserVo.getActivityId()));
}
@Override
public List<NutMap> unGroupUsers(Long activityId) {
Sql sql = Sqls.create("""
SELECT
au.*
FROM
act_user au
LEFT JOIN act_group_user agu ON au.userId = agu.userId
AND agu.activityId = @activityId
WHERE
au.activityId = @activityId
AND au.isSignIn = 1
AND agu.userId IS NULL
""");
sql.params().set("activityId", activityId);
return listMap(sql);
}
@Override
public List<NutMap> listGroupAndUsers(Long activityId) {
List<ActGroup> groups = dao().query(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", activityId));
Sql sql = Sqls.create("""
SELECT
u.*,
augu.groupId
FROM
act_group_user augu
LEFT JOIN act_user u ON u.userId = augu.userId
WHERE augu.activityId = @activityId
""");
sql.setParam("activityId", activityId);
List<NutMap> users = listMap(sql);
List<NutMap> groupFull = BeanUtil.copyToList(groups, NutMap.class);
for (NutMap group : groupFull) {
group.setv("members", users.stream().filter(v -> v.getLong("groupId") == group.getLong("id")).toList());
}
return groupFull;
}
}
@@ -0,0 +1,15 @@
package com.budwk.app.module.activity.service.impl;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.module.activity.models.ActRankScore;
import com.budwk.app.module.activity.service.ActRankScoreService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class ActRankScoreServiceImpl extends BaseServiceImpl<ActRankScore> implements ActRankScoreService {
public ActRankScoreServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,16 @@
package com.budwk.app.module.activity.vo;
import com.budwk.app.module.activity.models.ActGroupUser;
import lombok.Data;
import java.util.List;
@Data
public class ActManualGroupVo {
private Long id;
private Long groupName;
private List<ActGroupUser> members;
}
@@ -38,7 +38,8 @@ public class SysHomeController {
// return "beetl:/platform/sys/dashboard.html";
// }
// return "beetl:/platform/sys/home.html";
return "beetl:/platform/activity/manage/index.html";
// return "beetl:/platform/activity/manage/index.html";
return ">>:/platform/activity/manage";
}
@At("/403")
@@ -60,6 +60,6 @@
.el-button--primary {
color: #fff;
background-color: #1867b0;
border-color: #1867b0;
background-color: #3E54AC;
border-color: #3E54AC;
}
@@ -0,0 +1,23 @@
Vue.mixin({
mounted() {
if (this.$options.style) {
const styleId = `style-${this._uid}`
const style = document.createElement("style")
style.id = styleId
//支持函数和字符串
const css = typeof this.$options.style === "function" ? this.$options.style.call(this) : this.$options.style
style.textContent = css
document.head.appendChild(style)
}
},
beforeDestroy() {
if (this.$options.style) {
const styleId = `style-${this._uid}`
const style = document.getElementById(styleId)
if (style) {
document.head.removeChild(style)
}
}
}
})
@@ -0,0 +1,23 @@
Vue.mixin({
mounted() {
if (this.$options.style) {
const styleId = `style-${this._uid}`
const style = document.createElement("style")
style.id = styleId
//支持函数和字符串
const css = typeof this.$options.style === "function" ? this.$options.style.call(this) : this.$options.style
style.textContent = css
document.head.appendChild(style)
}
},
beforeDestroy() {
if (this.$options.style) {
const styleId = `style-${this._uid}`
const style = document.getElementById(styleId)
if (style) {
document.head.removeChild(style)
}
}
}
})
File diff suppressed because one or more lines are too long
@@ -39,8 +39,11 @@
<script src="${base!}/assets/platform/plugins/echarts/echarts.min.js"></script>
<!-- qrcode -->
<script src="${base!}/assets/platform/plugins/qrcode/qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.24.3/vuedraggable.umd.min.js"></script>
<!-- 自定义工具类 -->
<script src="${base!}/assets/platform/module/mixins/initTableMixins.js?v=1.0.0"></script>
<script src="${base!}/assets/platform/module/mixins/styleMixin.js"></script>
<!-- main -->
<script src="${base!}/assets/platform/js/main.js?v=1.0.1"></script>
@@ -58,14 +58,19 @@ layout("/layouts/platform_1.html"){
</div>
</div>
</el-card>
<el-card shadow="never" class="mt-3">
<el-row type="flex" style="justify-content: flex-start" class="mb-3">
<el-button type="primary" size="small" @click="openGrouping">开始分组</el-button>
<div v-if="unGroupUser && unGroupUser.length > 0" style="margin-left: 10px; display: inline-block;">
<el-button type="warning" size="small" style="position: relative;" @click="doUnGroupUserSetting">
未分组人员
<span class="un_group_user_span">{{ unGroupUser.length }}</span>
<!-- <div v-if="unGroupUser && unGroupUser.length > 0" style="margin-left: 10px; display: inline-block;">-->
<!-- <el-button type="warning" size="small" style="position: relative;" @click="doUnGroupUserSetting">-->
<!-- 未分组人员-->
<!-- <span class="un_group_user_span">{{ unGroupUser.length }}</span>-->
<!-- </el-button>-->
<!-- </div>-->
<div style="margin-left: 10px; display: inline-block;">
<el-button type="warning" size="small" style="position: relative;" @click="$refs.manualGroupRef.onOpen(pageForm.activityId)">
手动分组
</el-button>
</div>
</el-row>
@@ -96,16 +101,19 @@ layout("/layouts/platform_1.html"){
</el-table-column>
</el-table>
</el-card>
<group-setting ref="groupSettingRef" @success="successGroup"></group-setting>
<un-group-user ref="unGroupUserRef" @success="successGroup"></un-group-user>
<manual-group ref="manualGroupRef" @success="successGroup"></manual-group>
<view-info ref="viewInfoRef"></view-info>
</div>
<script>
<!--#include('groupSetting.js'){}#-->
<!--#include('unGroupUser.js'){}#-->
<!--#include('manualGroup.js'){}#-->
<!--#include('viewInfo.js'){}#-->
new Vue({
el: "#app",
@@ -113,6 +121,7 @@ layout("/layouts/platform_1.html"){
components: {
"group-setting": GROUP_SETTING,
"un-group-user": UN_GROUP_USER,
"manual-group": manualGroup,
"view-info": VIEW_INFO
},
data() {
@@ -0,0 +1,749 @@
const manualGroup = {
template: /*language=HTML*/ `
<el-dialog title="手动分组" :visible.sync="groupingDialog" width="80%" class="manual-group-dialog" fullscreen>
<div class="manual-group-content-wrap">
<div class="personnel-pool">
<div class="pool-header">
<h3>人员池</h3>
</div>
<div class="pool-content">
<div class="search-wrapper">
<i class="el-icon-search search-icon"></i>
<el-input
v-model="searchQuery"
placeholder="搜索人员..."
clearable>
</el-input>
</div>
<div class="personnel-list">
<draggable
:list="filteredPersonnel"
:group="{name:'people', pull: pullFunction, put: true}"
class="dragArea"
ghost-class="ghost"
@end="onDragEnd"
@add="onPersonnelAdd">
<div v-for="(person, index) in filteredPersonnel" :key="index" class="person-card">
<div class="person-header">
<div class="person-name">{{ person.userName }}</div>
<div class="person-gender" :class="person.sex === '男' ? 'male' : 'female'">
{{ person.sex }}
</div>
</div>
<div class="person-unit">{{ person.unitName }}</div>
</div>
<div v-if="filteredPersonnel.length === 0" class="no-members">
<i class="el-icon-warning-outline" style="margin-right: 5px;"></i> 无可用人员
</div>
</draggable>
</div>
<div class="stats">
<i class="el-icon-user stats-icon"></i>
共 {{ availablePersonnel.length }} 人可分配
</div>
</div>
</div>
<div class="groups-area">
<div class="group-container-header">
<h3>分组管理</h3>
<div class="create-text" @click="addNewGroup">
<i class="el-icon-plus"></i> 创建新分组
</div>
</div>
<div class="groups-container">
<div v-for="(group, groupIndex) in groups" :key="groupIndex" class="group-card">
<div class="group-header">
<div class="group-title">
<el-input
v-model="group.groupName"
placeholder="请输入组名"
size="small"
class="group-title-input">
</el-input>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
circle
@click="removeGroup(groupIndex)">
</el-button>
</div>
</div>
<div class="group-content">
<div>
<div class="group-members-header">
<div class="members-title">
队员 <span class="members-count">{{ group.members.length }}</span>
</div>
</div>
<draggable
v-model="group.members"
:group="{name: 'people', pull: true, put: true}"
class="group-members"
ghost-class="ghost"
@end="onDragEnd"
@remove="onMemberRemove">
<div v-for="(member, memberIndex) in group.members" :key="memberIndex"
class="person-card">
<div class="person-header">
<div class="person-name">{{ member.userName }}</div>
<div class="person-gender"
:class="member.sex === '男' ? 'male' : 'female'">{{ member.sex }}
</div>
</div>
<div class="person-unit">{{ member.unitName }}</div>
</div>
<div v-if="group.members.length === 0" class="no-members">
<i class="el-icon-d-arrow-left"></i> 拖动人员至此添加队员
</div>
</draggable>
</div>
</div>
</div>
<div v-if="groups.length === 0" class="empty-groups">
<i class="el-icon-collection empty-icon"></i>
<div class="empty-text">暂无分组,请点击上方"创建新分组"按钮开始组建团队</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="groupingDialog = false">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</template>
</el-dialog>
`,
data() {
return {
groupingDialog: false,
searchQuery: "",
availablePersonnel: [],
groups: [],
activityId: null
}
},
computed: {
filteredPersonnel() {
if (!this.searchQuery) {
return this.availablePersonnel
}
const query = this.searchQuery.toLowerCase()
return this.availablePersonnel.filter(person => {
return person.userName?.toLowerCase().includes(query) ||
person.unitName?.toLowerCase().includes(query)
})
}
},
methods: {
// 控制拖动行为
pullFunction() {
return "clone" // 使用克隆模式,这样拖动不会改变原始数组
},
async onOpen(activityId) {
this.groupingDialog = true
this.activityId = activityId
$.post("/platform/activity/group/unGroupUsers", { activityId }).then(res => {
if (res.code === 0) {
this.availablePersonnel = res.data
} else {
this.$message.error(res.msg)
}
})
$.post("/platform/activity/group/listGroup", { activityId }).then(res => {
if (res.code === 0) {
this.groups = res.data
} else {
this.$message.error(res.msg)
}
})
},
onSubmit() {
// 准备每个小组的数据
const groupData = this.groups.map(group => {
return {
id: group.id,
groupName: group.groupName,
groupUsers: group.members.map(member => ({
userId: member.userId,
groupId: group.id,
userName: member.userName
}))
}
})
// 打印每个小组的数据
console.log("分组数据:", JSON.stringify(groupData, null, 2))
// 检查是否有空组名
const emptyNameGroup = this.groups.find(group => !group.groupName || group.groupName.trim() === "")
if (emptyNameGroup) {
this.$message.warning("存在未命名的分组,请为所有分组命名")
return
}
// 检查重复组名
const duplicateNameGroup = this.groups.find(group => {
const groupName = group.groupName.trim()
return groupName && this.groups.some(g => g !== group && g.groupName.trim() === groupName)
})
if (duplicateNameGroup) {
this.$message.warning("存在重复的分组名称,请修改分组名称")
return
}
// 检查是否有空组
const emptyGroup = this.groups.find(group => group.members.length === 0)
if (emptyGroup) {
this.$confirm("分组" + emptyGroup.groupName + "中没有成员,是否继续保存?", "提示", {
confirmButtonText: "继续保存",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.saveGroups(groupData)
}).catch(() => {
})
} else {
this.saveGroups(groupData)
}
},
async saveGroups(groupData) {
// 显示加载状态
const loading = this.$loading({
lock: true,
text: "保存中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)"
})
const { code, msg } = await $.post("/platform/activity/group/manualSetGroup", {
actGroups: JSON.stringify(groupData),
activityId: this.activityId
})
loading.close()
if (code === 0) {
this.groupingDialog = false
this.$message.success(msg)
this.$emit("success")
} else {
this.$message.error(msg)
}
},
onDragEnd(evt) {
if (evt.from.className.includes("dragArea") && evt.to.className.includes("group-members")) {
// 从人员池拖到分组时,需要从availablePersonnel中移除
const draggedPerson = this.filteredPersonnel[evt.oldIndex]
const indexInAvailable = this.availablePersonnel.findIndex(p => p.userId === draggedPerson.userId)
if (indexInAvailable > -1) {
this.availablePersonnel.splice(indexInAvailable, 1)
}
// 设置目标组的groupId
const targetGroup = this.findGroupByDomElement(evt.to)
if (targetGroup) {
// 找到刚刚拖入的成员并设置其groupId
const newIndex = evt.newIndex
if (targetGroup.members[newIndex]) {
targetGroup.members[newIndex].groupId = targetGroup.id
}
}
} else if (evt.from.className.includes("group-members") && evt.to.className.includes("group-members")) {
// 在分组之间拖动时,确保正确设置groupId
const targetGroup = this.findGroupByDomElement(evt.to)
if (targetGroup) {
// 找到刚刚拖入的成员并设置其groupId
const newIndex = evt.newIndex
if (targetGroup.members[newIndex]) {
targetGroup.members[newIndex].groupId = targetGroup.id
}
}
}
// 确保所有组内都没有重复的人员
this.$nextTick(() => {
this.removeDuplicates()
})
},
removeDuplicates() {
// 收集所有已分配人员的ID
const assignedIds = new Set()
// 检查每个组的成员
this.groups.forEach(group => {
// 检查组内成员是否有重复
const uniqueMembers = []
group.members.forEach(member => {
if (!assignedIds.has(member.userId)) {
assignedIds.add(member.userId)
uniqueMembers.push(member)
}
})
group.members = uniqueMembers
})
// 更新availablePersonnel,移除已分配的人员
this.availablePersonnel = this.availablePersonnel.filter(person => !assignedIds.has(person.userId))
},
addNewGroup() {
this.groups.push({
id: Date.now(), // 临时ID,后端会重新分配
groupName: "分组 " + (this.groups.length + 1),
activityId: this.activityId,
members: []
})
this.$notify({
title: "成功",
message: "已创建新分组",
type: "success"
})
},
removeGroup(index) {
this.$confirm("确定要删除该分组吗?所有队员将返回人员池", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
// 将组内人员返回到人员池
const group = this.groups[index]
group.members.forEach(member => {
this.availablePersonnel.push({ ...member })
})
// 删除组
this.groups.splice(index, 1)
this.$message({
type: "success",
message: "删除成功!"
})
}).catch(() => {
// 取消删除
})
},
onPersonnelAdd(evt) {
// 当成员被拖回人员池时,会触发此方法
// 不需要特殊处理,因为:
// 1. Vue自动处理了filteredPersonnel的更新
// 2. 源组的成员会在拖动结束时自动移除
// 3. removeDuplicates方法会确保没有重复
// 确保availablePersonnel同步更新
if (!this.availablePersonnel.some(p => p.userId === this.filteredPersonnel[evt.newIndex].userId)) {
this.availablePersonnel.push(this.filteredPersonnel[evt.newIndex])
}
},
findGroupByDomElement(element) {
// 通过DOM元素查找对应的分组数据
const groupIndex = this.findGroupIndexByDomElement(element)
return groupIndex !== -1 ? this.groups[groupIndex] : null
},
findGroupIndexByDomElement(element) {
// 查找包含该DOM元素的分组索引
const groupCard = this.findParentWithClass(element, "group-card")
if (!groupCard) return -1
const groupCards = Array.from(document.querySelectorAll(".group-card"))
return groupCards.indexOf(groupCard)
},
findParentWithClass(element, className) {
// 查找具有特定类名的父元素
let parent = element
while (parent && !parent.classList.contains(className)) {
parent = parent.parentElement
}
return parent
},
onMemberRemove(evt) {
// 当成员被从分组移除时触发(可能是拖到人员池或其他分组)
// 如果是拖到人员池,我们需要确保availablePersonnel中添加了这个成员
const groupIndex = this.findGroupIndexByDomElement(evt.from)
}
},
style: /*language=CSS*/ `
.manual-group-dialog .el-dialog__body {
padding: 0;
}
.manual-group-content-wrap {
display: flex;
padding: 20px;
gap: 20px;
min-height: 500px;
height: calc(100vh - 115px);
overflow: auto;
}
.personnel-pool {
width: 300px;
flex-shrink: 0;
background: white;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: all 0.3s ease;
height: fit-content;
max-height: 100%;
display: flex;
flex-direction: column;
}
.pool-header, .group-container-header {
background-color: #3E54AC;
color: white;
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
height: 44px;
}
.pool-header h3, .group-container-header h3 {
font-size: 16px;
font-weight: 500;
margin: 0;
}
.pool-content {
padding: 15px;
overflow: hidden;
flex-grow: 1;
display: flex;
flex-direction: column;
max-height: 100%;
}
.person-card {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
cursor: move;
border-left: 3px solid #7286D3;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.person-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(114, 134, 211, 0.12), transparent);
opacity: 0;
transition: all 0.3s ease;
}
.person-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
}
.person-card:hover::before {
opacity: 1;
}
.person-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.person-name {
font-weight: 600;
font-size: 14px;
color: #2B3A55;
}
.person-gender {
display: inline-block;
padding: 2px 8px;
border-radius: 20px;
font-size: 12px;
color: white;
font-weight: 500;
}
.male {
background-color: #7286D3;
}
.female {
background-color: #F87474;
}
.person-unit {
color: #64748b;
font-size: 12px;
display: flex;
align-items: center;
}
.person-unit::before {
content: '';
display: inline-block;
width: 5px;
height: 5px;
background-color: #64748b;
border-radius: 50%;
margin-right: 6px;
}
.groups-area {
flex: 1;
display: flex;
flex-direction: column;
border-radius: 10px;
overflow: hidden;
}
.groups-container {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
grid-auto-rows: min-content;
align-content: start;
align-items: start;
justify-items: stretch;
grid-auto-flow: row;
overflow-y: auto;
padding: 20px;
background-color: #f8fafc;
}
.group-card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: all 0.3s ease;
max-height: 500px;
display: flex;
flex-direction: column;
border-top: 5px solid #3E54AC;
position: relative;
width: 100%;
margin: 0;
}
.group-header {
padding: 15px;
background-color: var(--primary-light);
flex-shrink: 0;
}
.group-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.group-title-input .el-input__inner {
border: none;
background: transparent;
font-size: 16px;
font-weight: 600;
color: #2B3A55;
height: 36px;
padding: 0;
}
.group-title-input .el-input__inner:focus {
box-shadow: none;
}
.group-content {
padding: 15px;
overflow-y: auto;
flex: 1;
min-height: 150px;
/*max-height: 384px;*/
}
.group-leader {
background: #f0f9ff;
border-radius: 8px;
padding: 12px;
margin-bottom: 15px;
position: relative;
border-left: 3px solid #36AE7C;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.05);
}
.leader-badge {
position: absolute;
top: -8px;
right: -8px;
background-color: #36AE7C;
color: white;
border-radius: 20px;
padding: 2px 10px;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.group-members-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.members-title {
font-size: 15px;
font-weight: 600;
color: #2B3A55;
display: flex;
align-items: center;
}
.members-count {
background-color: var(--primary-light);
color: #3E54AC;
border-radius: 20px;
padding: 2px 8px;
font-size: 12px;
margin-left: 6px;
}
.group-members {
min-height: 50px;
padding: 8px 0;
/*max-height: 250px;*/
overflow-y: auto;
}
.ghost {
opacity: 0.4;
background: #e0f2fe !important;
border: 1px dashed #7286D3 !important;
}
.no-members {
color: #94a3b8;
text-align: center;
padding: 20px 0;
border: 2px dashed #e2e8f0;
border-radius: 8px;
font-size: 13px;
}
.stats {
margin-top: 12px;
color: #64748b;
font-size: 12px;
padding: 8px 12px;
background-color: var(--primary-light);
border-radius: 8px;
display: flex;
align-items: center;
flex-shrink: 0;
}
.stats-icon {
margin-right: 8px;
color: #3E54AC;
}
.search-wrapper {
position: relative;
margin-bottom: 12px;
flex-shrink: 0;
}
.search-wrapper .el-input__inner {
border-radius: 25px;
padding-left: 35px;
background-color: #f8fafc;
border: 1px solid #e2e8f0;
height: 36px;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
z-index: 2;
}
.create-text {
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s ease;
padding: 0 10px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.15);
height: 24px; /* 与header高度匹配 */
}
.create-text:hover {
background-color: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
.create-text:active {
background-color: rgba(255, 255, 255, 0.3);
transform: translateY(0);
}
.create-text i {
margin-right: 5px;
font-size: 14px;
}
.group-leader, .leader-badge, .leader-actions {
display: none;
}
.empty-groups {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 0;
width: 100%;
text-align: center;
}
.empty-icon {
font-size: 50px;
color: #cbd5e1;
margin-bottom: 15px;
}
.empty-text {
color: #64748b;
font-size: 15px;
margin-bottom: 15px;
}
.personnel-list {
overflow-y: auto;
flex-grow: 1;
padding-right: 5px;
}
`
}
@@ -0,0 +1,368 @@
<!--#
layout("/layouts/platform_1.html"){
#-->
<style>
.el-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
}
.el-card__header {
background: #3E54AC;
color: white;
font-size: 16px;
font-weight: bold;
padding: 15px 20px;
border-bottom: 1px solid #ebeef5;
}
.el-table {
margin: 15px 0;
border-radius: 5px;
overflow: hidden;
}
.rank-medal {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
border-radius: 50%;
margin-right: 8px;
color: white;
font-weight: bold;
}
.rank-medal-1 {
background-color: #f5c518;
box-shadow: 0 2px 4px rgba(245, 197, 24, 0.4);
}
.rank-medal-2 {
background-color: #c0c0c0;
box-shadow: 0 2px 4px rgba(192, 192, 192, 0.4);
}
.rank-medal-3 {
background-color: #cd7f32;
box-shadow: 0 2px 4px rgba(205, 127, 50, 0.4);
}
.score-value {
font-weight: bold;
color: #409EFF;
}
.el-form-item__label {
font-weight: bold;
}
.empty-data {
text-align: center;
padding: 40px 0;
color: #909399;
}
.empty-data i {
font-size: 50px;
margin-bottom: 10px;
}
.action-buttons {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
}
.batch-actions {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
</style>
<div id="app">
<el-card shadow="hover" class="" style="width: 800px;margin: 0 auto">
<div slot="header" class="clearfix">
<span><i class="el-icon-trophy"></i> 名次积分管理</span>
</div>
<div class="action-buttons">
<el-button-group>
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAddMultiple">批量添加</el-button>
<el-button type="success" size="small" icon="el-icon-refresh" @click="doSearch">刷新数据</el-button>
</el-button-group>
</div>
<el-table v-loading="tableLoading" :data="tableData" border stripe>
<el-table-column label="名次">
<template slot-scope="scope">
<template v-if="scope.row.ranking === '第一名' || scope.row.ranking === '冠军' || scope.row.ranking === '1'">
<span class="rank-medal rank-medal-1">1</span>
</template>
<template v-else-if="scope.row.ranking === '第二名' || scope.row.ranking === '亚军' || scope.row.ranking === '2'">
<span class="rank-medal rank-medal-2">2</span>
</template>
<template v-else-if="scope.row.ranking === '第三名' || scope.row.ranking === '季军' || scope.row.ranking === '3'">
<span class="rank-medal rank-medal-3">3</span>
</template>
<span>{{ scope.row.ranking }}</span>
</template>
</el-table-column>
<el-table-column label="积分">
<template slot-scope="scope">
<span class="score-value">{{ scope.row.score }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template slot-scope="scope">
<el-button size="mini" type="primary" icon="el-icon-edit" circle @click="handleEdit(scope.row)"></el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" circle @click="handleDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
<template v-if="tableData.length === 0">
<div class="empty-data">
<i class="el-icon-document"></i>
<p>暂无名次积分数据,请点击"批量添加"按钮添加</p>
</div>
</template>
</el-card>
<el-dialog title="编辑名次积分" :visible.sync="editDialogVisible" width="30%">
<el-form :model="editForm" :rules="rules" ref="editForm" label-width="80px">
<el-form-item label="名次" prop="ranking">
<el-input v-model="editForm.ranking" placeholder="请输入名次,可以是中文" prefix-icon="el-icon-trophy"></el-input>
</el-form-item>
<el-form-item label="积分" prop="score">
<el-input-number v-model="editForm.score" :precision="2" :step="1" :min="0"
style="width: 100%"></el-input-number>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEditForm">确定</el-button>
</span>
</el-dialog>
<el-dialog title="批量添加名次积分" :visible.sync="batchAddDialogVisible" width="800px">
<el-form :model="batchForm" ref="batchForm">
<div class="batch-actions">
<div>
<el-button type="primary" size="small" icon="el-icon-plus" @click="addBatchRow">添加一行</el-button>
<el-button type="danger" size="small" icon="el-icon-delete" @click="clearBatchRows">清空</el-button>
</div>
<el-button-group>
<el-button size="small" type="primary" plain @click="addPresetRanks('123')">添加前三名</el-button>
<el-button size="small" type="primary" plain @click="addPresetRanks('champion')">添加冠亚季军</el-button>
</el-button-group>
</div>
<el-table :data="batchForm.items" border style="width: 100%" stripe>
<el-table-column label="名次" width="180">
<template slot-scope="scope">
<el-form-item :prop="'items.' + scope.$index + '.ranking'" :rules="rules.ranking"
style="margin-bottom: 0;">
<el-input v-model="scope.row.ranking" placeholder="请输入名次" prefix-icon="el-icon-trophy"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="积分">
<template slot-scope="scope">
<el-form-item :prop="'items.' + scope.$index + '.score'" :rules="rules.score"
style="margin-bottom: 0;">
<el-input-number v-model="scope.row.score" :precision="2" :step="1" :min="0"
controls-position="right" style="width: 100%"></el-input-number>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button size="mini" type="danger" icon="el-icon-delete" circle
@click="removeBatchRow(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-2" v-if="batchError">
<el-alert :title="batchError" type="error" show-icon></el-alert>
</div>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="batchAddDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitBatchForm">确定</el-button>
</span>
</el-dialog>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
editDialogVisible: false,
batchAddDialogVisible: false,
editForm: {
id: null,
ranking: "",
score: 0
},
batchForm: {
items: []
},
batchError: "",
rules: {
ranking: [
{ required: true, message: "请输入名次", trigger: "blur" }
],
score: [
{ required: true, message: "请输入积分", trigger: "blur" },
{ type: "number", message: "积分必须是数字", trigger: "blur" }
]
},
editIndex: -1
}
},
methods: {
handleAddMultiple() {
this.batchForm.items = [this.createEmptyBatchItem()]
this.batchError = ""
this.batchAddDialogVisible = true
},
createEmptyBatchItem() {
return {
id: null,
ranking: "",
score: 0
}
},
addBatchRow() {
this.batchForm.items.push(this.createEmptyBatchItem())
},
removeBatchRow(index) {
this.batchForm.items.splice(index, 1)
},
clearBatchRows() {
this.batchForm.items = []
},
addPresetRanks(type) {
if (type === '123') {
this.batchForm.items = [
{ id: null, ranking: "第一名", score: 100 },
{ id: null, ranking: "第二名", score: 80 },
{ id: null, ranking: "第三名", score: 60 }
]
} else if (type === 'champion') {
this.batchForm.items = [
{ id: null, ranking: "冠军", score: 100 },
{ id: null, ranking: "亚军", score: 80 },
{ id: null, ranking: "季军", score: 60 }
]
}
},
handleEdit(row) {
this.editForm = JSON.parse(JSON.stringify(row))
this.editIndex = this.tableData.findIndex(item => item.id === row.id)
this.editDialogVisible = true
},
handleDelete(row) {
this.$confirm("确定要删除这条记录吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
if (row.id) {
$.post("/platform/activity/rankScore/delete", { id: row.id }).then(res => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.error(res.msg)
}
})
} else {
const index = this.tableData.findIndex(item => item === row)
if (index !== -1) {
this.tableData.splice(index, 1)
this.$message.success("删除成功")
}
}
}).catch(() => {
})
},
submitEditForm() {
this.$refs.editForm.validate((valid) => {
if (valid) {
$.post("/platform/activity/rankScore/update").then(res => {
if (res.code === 0) {
this.$message.success("修改成功")
this.editDialogVisible = false
this.pageData()
} else {
this.$message.error(res.msg)
}
})
}
})
},
submitBatchForm() {
if (this.batchForm.items.length === 0) {
this.$message.error("请至少添加一条记录")
return
}
this.$refs.batchForm.validate((valid) => {
if (!valid) {
return
}
// 检查新添加的记录中是否有重复的名次
const allRankings = this.batchForm.items.map(item => item.ranking)
const uniqueRankings = [...new Set(allRankings)]
if (allRankings.length !== uniqueRankings.length) {
this.batchError = "批量添加的数据中存在重复的名次,请检查"
return
}
$.post("/platform/activity/rankScore/save", { actRankScore: JSON.stringify(this.batchForm.items) }).then(res => {
if (res.code === 0) {
this.$message.success("保存成功")
this.batchAddDialogVisible = false
this.pageData()
} else {
this.$message.error(res.msg)
}
})
})
},
save() {
// 检查是否有重复的名次
const rankings = this.tableData.map(item => item.ranking)
const uniqueRankings = [...new Set(rankings)]
if (rankings.length !== uniqueRankings.length) {
this.$message.error("存在重复的名次,请检查后重新提交")
return
}
$.post("/platform/activity/rankScore/save", { actRankScore: JSON.stringify(this.tableData) }).then(res => {
if (res.code === 0) {
this.$message.success("保存成功")
this.pageData()
} else {
this.$message.error(res.msg)
}
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,551 @@
<!--#
layout("/layouts/platform_1.html"){
#-->
<div id="app">
<el-card shadow="never" class="filter-card">
<div class="search">
<div class="search-item">
<div class="search-item-label">年度</div>
<div class="search-item-option">
<el-date-picker
:clearable="false"
placeholder="选择年"
style="width: 100%"
@change="listAct"
type="year"
v-model="pageForm.year"
value-format="yyyy"
></el-date-picker>
</div>
</div>
<div class="search-item">
<div class="search-item-label">所属活动</div>
<div class="search-item-option">
<el-select v-model="pageForm.activityId" placeholder="请选择活动" @change="activityChange">
<el-option
v-for="item in activityOptions"
:key="item.id"
:label="item.title"
:value="item.id">
</el-option>
</el-select>
</div>
</div>
<div class="search-query">
<el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
</div>
</div>
</el-card>
<div class="score-entry-container mt-3">
<div class="score-entry-header">
<div class="header-title">活动项目名次设置</div>
</div>
<div v-if="!pageForm.activityId" class="empty-state">
<i class="el-icon-data-analysis empty-icon"></i>
<div class="empty-text">请先选择活动</div>
</div>
<div v-else-if="itemOptions.length === 0" class="empty-state">
<i class="el-icon-collection empty-icon"></i>
<div class="empty-text">暂无项目数据</div>
</div>
<div v-else class="score-tabs-container">
<el-tabs type="border-card" class="custom-tabs">
<el-tab-pane v-for="item in itemOptions" :key="item.id" :label="item.name">
<div class="item-info">
<div class="item-details">
<div class="detail-item">
<i class="el-icon-aim detail-icon"></i>
<span class="detail-label">项目形式:</span>
<span class="detail-content">{{item.form}}</span>
</div>
<div class="detail-item">
<i class="el-icon-set-up detail-icon"></i>
<span class="detail-label">项目规则:</span>
<span class="detail-content">{{item.rule}}</span>
</div>
</div>
<div class="item-actions">
<el-button type="primary" size="small" @click="saveItemScores(item.id)" :loading="item.saving">
<i class="el-icon-check"></i> 保存此项目分数
</el-button>
</div>
</div>
<div class="teams-table-container">
<el-table
:data="groupOptions"
border
style="width: 100%"
v-loading="loading"
:header-cell-style="{background:'#f5f7fa', color: '#606266'}"
class="custom-table">
<el-table-column label="队伍名称" width="150">
<template slot-scope="scope">
<div class="team-name">
<i class="el-icon-user-solid team-icon"></i>
{{ scope.row.groupName }}
</div>
</template>
</el-table-column>
<el-table-column label="名次设置" width="220">
<template slot-scope="scope">
<el-select
v-model="getScoreRecord(item.id, scope.row.id).rankingObj"
placeholder="请选择名次"
@change="updateScore(item.id, scope.row.id)"
class="rank-select">
<el-option
v-for="(rank, index) in rankScoreOptions"
:key="rank.id"
:label="rank.ranking"
:value="index + 1">
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="得分">
<template slot-scope="scope">
<div class="score-value"
:class="{'has-score': getScoreRecord(item.id, scope.row.id).score}">
{{ getScoreRecord(item.id, scope.row.id).score || '-' }}
</div>
</template>
</el-table-column>
<el-table-column label="备注" width="250">
<template slot-scope="scope">
<el-input
v-model="getScoreRecord(item.id, scope.row.id).remark"
placeholder="请输入备注"
type="textarea"
class="remark-input"
:rows="2">
</el-input>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
<script>
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
pageForm: {
year: new Date().getFullYear() + ""
},
activityOptions: [],
groupOptions: [],
itemOptions: [],
itemScoreList: [],
rankScoreOptions: [],
loading: false
}
},
methods: {
// 获取活动
listAct() {
$.get("/platform/activity/scoreEntry/list-activity", this.pageForm).then(res => {
if (res.code === 0) {
this.activityOptions = res.data
if (this.activityOptions.length > 0) {
this.pageForm.activityId = this.activityOptions[0].id
this.activityChange()
}
} else {
this.$message.error(res.msg)
}
})
},
// 获取队伍
async listGroup() {
const {
code,
data,
msg
} = await $.get("/platform/activity/scoreEntry/list-group/" + this.pageForm.activityId)
if (code === 0) {
this.groupOptions = data
} else {
this.$message.error(msg)
}
},
// 获取项目
async listItem() {
const {
code,
data,
msg
} = await $.get("/platform/activity/scoreEntry/list-item/" + this.pageForm.activityId)
if (code === 0) {
// 添加保存状态标志
this.itemOptions = data.map(item => ({
...item,
saving: false
}))
} else {
this.$message.error(msg)
}
},
// 获取名次积分对应
async listRankScore() {
const {
code,
data,
msg
} = await $.get("/platform/activity/scoreEntry/list-rank-score")
if (code === 0) {
this.rankScoreOptions = data
} else {
this.$message.error(msg)
}
},
// 获取项目得分列表
async listItemScore() {
const {
code,
data,
msg
} = await $.get("/platform/activity/scoreEntry/list-item-score/" + this.pageForm.activityId)
if (code === 0) {
this.itemScoreList = data
// 为每条记录添加rankingObj属性,用于下拉框绑定
this.itemScoreList.forEach(score => {
score.rankingObj = score.ranking || null
})
} else {
this.$message.error(msg)
}
},
// 获取特定项目和队伍的得分记录
getScoreRecord(itemId, groupId) {
// 在已有的记录中查找
let record = this.itemScoreList.find(score =>
score.itemId === itemId && score.groupId === groupId
)
// 如果没有找到,创建一个新记录
if (!record) {
record = {
activityId: this.pageForm.activityId,
itemId: itemId,
groupId: groupId,
score: null,
ranking: null,
rankingObj: null,
remark: ""
}
this.itemScoreList.push(record)
}
// 确保remark字段存在
if (!record.hasOwnProperty("remark")) {
record.remark = ""
}
// 确保rankingObj字段存在
if (!record.hasOwnProperty("rankingObj")) {
record.rankingObj = record.ranking
}
return record
},
// 更新分数
updateScore(itemId, groupId) {
const record = this.getScoreRecord(itemId, groupId)
const rankingValue = record.rankingObj
// 同步ranking字段和rankingObj字段
record.ranking = rankingValue
if (rankingValue) {
// 查找对应名次的分数
const rankScore = this.rankScoreOptions[rankingValue - 1]
if (rankScore) {
record.score = rankScore.score
}
} else {
record.score = null
}
},
// 保存单个项目的分数
async saveItemScores(itemId) {
if (!this.pageForm.activityId) {
this.$message.warning("请先选择活动")
return
}
// 找到当前项目
const currentItem = this.itemOptions.find(item => item.id === itemId)
if (!currentItem) return
// 设置保存状态
currentItem.saving = true
try {
// 过滤出当前项目的有效分数记录
const validScores = this.itemScoreList.filter(score =>
score.itemId === itemId && score.ranking !== null
)
// 转换为后端需要的数据格式
const postData = validScores.map(score => ({
id: score.id,
activityId: score.activityId,
itemId: score.itemId,
groupId: score.groupId,
score: score.score,
ranking: score.ranking
}))
const { code, msg } = await $.post("/platform/activity/scoreEntry/save-score", {
scores: JSON.stringify(postData)
})
if (code === 0) {
this.$message.success(currentItem.name+'项目分数保存成功')
await this.listItemScore()
} else {
this.$message.error(msg || "保存失败")
}
} catch (error) {
console.error(error)
this.$message.error("保存失败:" + (error.message || "未知错误"))
} finally {
currentItem.saving = false
}
},
// 活动改变
async activityChange() {
this.loading = true
try {
await this.listGroup()
await this.listItem()
await this.listItemScore()
} catch (error) {
console.error(error)
} finally {
this.loading = false
}
},
// 搜索
doSearch() {
if (this.pageForm.activityId) {
this.activityChange()
} else {
this.$message.warning("请先选择活动")
}
}
},
created() {
this.listAct()
this.listRankScore()
}
})
</script>
<style>
.filter-card {
margin-bottom: 20px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
}
.score-entry-container {
background: white;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.score-entry-header {
background-color: #3E54AC;
color: white;
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
height: 55px;
}
.header-title {
font-size: 16px;
font-weight: 500;
margin: 0;
}
.custom-tabs {
box-shadow: none !important;
border: none !important;
}
.custom-tabs .el-tabs__header {
background-color: #f8fafc;
padding: 0 15px;
margin: 0;
}
.custom-tabs .el-tabs__item {
height: 50px;
line-height: 50px;
font-size: 14px;
color: #64748b;
}
.custom-tabs .el-tabs__item.is-active {
color: #3E54AC;
font-weight: 600;
}
.custom-tabs .el-tabs__nav-wrap::after {
background-color: #e2e8f0;
height: 1px;
}
.custom-tabs .el-tabs__active-bar {
background-color: #3E54AC;
height: 3px;
}
.item-info {
background-color: #f8fafc;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.item-details {
line-height: 1.6;
flex: 1;
}
.item-actions {
margin-left: 15px;
flex-shrink: 0;
}
.detail-item {
margin-bottom: 8px;
display: flex;
align-items: flex-start;
}
.detail-icon {
color: #3E54AC;
margin-right: 5px;
font-size: 16px;
margin-top: 3px;
}
.detail-label {
font-weight: 600;
color: #2B3A55;
margin-right: 5px;
}
.detail-content {
color: #64748b;
flex: 1;
}
.teams-table-container {
margin-top: 15px;
}
.custom-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.team-name {
display: flex;
align-items: center;
font-weight: 500;
color: #2B3A55;
}
.team-icon {
color: #7286D3;
margin-right: 8px;
font-size: 16px;
}
.rank-select {
width: 100%;
}
.rank-select .el-input__inner {
border-radius: 4px;
}
.score-value {
text-align: center;
font-size: 16px;
color: #64748b;
}
.score-value.has-score {
color: #36AE7C;
font-weight: 600;
}
.remark-input .el-textarea__inner {
border-radius: 4px;
resize: none;
border-color: #e2e8f0;
}
.remark-input .el-textarea__inner:focus {
border-color: #7286D3;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
text-align: center;
}
.empty-icon {
font-size: 60px;
color: #cbd5e1;
margin-bottom: 20px;
}
.empty-text {
color: #64748b;
font-size: 16px;
}
.el-tabs__content {
padding: 20px;
}
</style>
<!--#
}
#-->
@@ -97,20 +97,20 @@ layout("/layouts/platform_1.html"){
{{row.isStay?'住宿':'不住宿'}}
</template>
</el-table-column>
<el-table-column label="物品发放" prop="isGrant">
<template slot-scope="scope">
<el-tag v-if="scope.row.isGrant" type="success">已发放</el-tag>
<el-tag v-else type="danger">未发放</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200">
<template #default="{row}">
<el-button v-if="!row.isGrant || row.isGrant === 0" type="primary" size="mini"
@click="doGrant(row, true)">设为已发放物品
</el-button>
<el-button v-else type="danger" size="mini" @click="doGrant(row, false)">设为未发放物品</el-button>
</template>
</el-table-column>
<!-- <el-table-column label="物品发放" prop="isGrant">-->
<!-- <template slot-scope="scope">-->
<!-- <el-tag v-if="scope.row.isGrant" type="success">已发放</el-tag>-->
<!-- <el-tag v-else type="danger">未发放</el-tag>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column label="操作" fixed="right" width="200">-->
<!-- <template #default="{row}">-->
<!-- <el-button v-if="!row.isGrant || row.isGrant === 0" type="primary" size="mini"-->
<!-- @click="doGrant(row, true)">设为已发放物品-->
<!-- </el-button>-->
<!-- <el-button v-else type="danger" size="mini" @click="doGrant(row, false)">设为未发放物品</el-button>-->
<!-- </template>-->
<!-- </el-table-column>-->
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
@@ -76,6 +76,7 @@ layout("/layouts/platform_1.html"){
</el-row>
<el-table v-loading="tableLoading" show-overflow-tooltip :data="tableData">
<el-table-column label="序号" type="index" header-align="center" align="center"
:index="indexMethod"
width="50"></el-table-column>
<el-table-column label="姓名" prop="userName"></el-table-column>
<el-table-column label="性别" prop="sex"></el-table-column>
@@ -93,6 +94,7 @@ layout("/layouts/platform_1.html"){
<el-table-column label="操作" fixed="right" width="300">
<template #default="scope">
<el-button type="primary" size="mini" @click="proxySignIn(scope.row.id)" v-if="!scope.row.isSignIn">签到</el-button>
<el-button type="primary" size="mini" @click="cancelSignIn(scope.row.id)" v-if="scope.row.isSignIn">取消签到</el-button>
<el-button type="primary" size="mini" @click="openModify(scope.row)">修改</el-button>
<el-button type="danger" size="mini" @click="doRemove(scope.row.id)">删除</el-button>
</template>
@@ -466,7 +468,22 @@ layout("/layouts/platform_1.html"){
this.doSearch()
this.$message.success(resp.msg)
} else {
this.$message.warning('操作失败')
this.$message.warning(resp.msg)
}
})
},
cancelSignIn(id){
this.$confirm('您确定要取消签到吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const resp = await $.post('/platform/activity/user/cancelSignIn', {id})
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
}else{
this.$message.warning(resp.msg)
}
})
},
@@ -18,7 +18,7 @@ layout("/layouts/platform_1.html"){
</div>
</el-card>
<el-card shadow="never" class="mt-3">
<el-table :key="tableKey" :data="tableData" style="width: 100%" size="small"
<el-table :key="tableKey" :data="tableData" style="width: 100%"
:highlight-current-row="true" row-key="id" lazy
:load="loadChild">
<el-table-column label="菜单名称" header-align="center" prop="name"
+66 -6
View File
@@ -1,9 +1,18 @@
package com.budwk.app;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.utils.PwdUtil;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import lombok.Data;
import org.apache.poi.ss.usermodel.Workbook;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.nutz.boot.test.junit4.NbJUnit4Runner;
@@ -11,8 +20,14 @@ import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@IocBean
@RunWith(NbJUnit4Runner.class)
@@ -24,7 +39,7 @@ public class Test1 {
private SysDictService sysDictService;
@Test
public void to01(){
public void to01() {
String str = """
'旅行', '摄影', '音乐', '电影', '美食', '健身', '阅读', '跑步','游泳', '爬山', '滑雪', '烹饪', '绘画', '书法', '手工', '舞蹈','瑜伽', '钢琴', '吉他', '写作', '篮球', '足球', '网球', '羽毛球','动漫', '游戏', '宠物', '园艺', '咖啡', '茶艺', '插花', '收藏','志愿服务', '数码', '科技', '投资', '理财', '历史', '哲学', '心理学'
""";
@@ -42,11 +57,56 @@ public class Test1 {
}
@Test
public void t002(){
String salt = R.UU32();
System.out.println(salt);
String password = PwdUtil.getPassword("dd3s@#2025", salt);
System.out.println(password);
public void t002() {
List<ExcelUser> list = ExcelImportUtil.importExcel(new File("C:\\Users\\jug\\Desktop\\工作簿3.xlsx"), ExcelUser.class, new ImportParams());
List<ExcelUser> list2 = ExcelImportUtil.importExcel(new File("D:\\downloads\\会员名单 (3).xls"), ExcelUser.class, new ImportParams());
List<NutMap> changeUsers = new ArrayList<>();
// 找出list中的人员在list2中单位不一样的数据
list.forEach(e -> {
Optional<ExcelUser> first = list2.stream().filter(e2 -> e2.getGh().equals(e.getGh())).findFirst();
if (first.isPresent()) {
String dw = StrUtil.blankToDefault(first.get().getDw(), "");
if (!dw.equals(e.getDw())) {
// System.out.println("姓名:" + e.getName() + ",工号:" + e.getGh() + ",原单位:" + e.getDw() + ",现单位:" + dw);
changeUsers.add(NutMap.NEW()
.addv("name", e.getName())
.addv("gh", e.getGh())
.addv("ydw", e.getDw())
.addv("xdw", dw));
}
}
});
ArrayList<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("姓名", "name"));
entities.add(new ExcelExportEntity("工号", "gh"));
entities.add(new ExcelExportEntity("原单位", "ydw"));
entities.add(new ExcelExportEntity("现单位", "xdw"));
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), entities, changeUsers);
try {
FileOutputStream outputStream = new FileOutputStream("C:\\Users\\jug\\Desktop\\单位变动人员.xlsx");
workbook.write(outputStream);
}catch (Exception e){
e.printStackTrace();
}
}
@Data
public static class ExcelUser {
@Excel(name = "工号")
private String gh;
@Excel(name = "姓名")
private String name;
@Excel(name = "单位")
private String dw;
}
}
@@ -1,5 +1,41 @@
<template>
<view class="page-container">
<!-- 人员选项卡区域 -->
<view class="tabs-container">
<view class="tabs-header">
<view class="tabs-title">
<text class="title-text">报名人员</text>
<view class="header-actions">
<view class="person-counter">
<text class="counter-text">{{ formData.userList.length }}人</text>
</view>
<view class="header-add-btn" @click="addNewUser">
<t-icon name="add" size="16px"></t-icon>
</view>
</view>
</view>
</view>
<scroll-view scroll-x="true" class="tabs-scroll-view" show-scrollbar="false">
<view class="tabs">
<view
v-for="(user, index) in formData.userList"
:key="index"
class="tab"
:class="{ active: activeTabIndex === index }"
@click="switchTab(index)"
>
<view class="tab-content">
<view class="tab-icon" v-if="activeTabIndex === index">
<t-icon name="user" size="16px"></t-icon>
</view>
<text class="tab-text">{{ user.userName || `人员${index + 1}` }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="tabs-indicator"></view>
</view>
<view class="form-area">
<t-form
ref="formRef"
@@ -9,6 +45,7 @@
label-align="left"
scroll-to-first-error="auto"
labelWidth="90px"
:requiredMark="true"
>
<!-- <view class="form-block">-->
<!-- <view class="form-block-title">-->
@@ -29,20 +66,19 @@
<view class="form-block">
<view class="form-block-title">
<t-icon name="usergroup"></t-icon>
报名人员信息
</view>
<view v-for="(user,index) in formData.userList" :key="index" class="user-card">
<view class="user-card-header">
<view class="user-card-header-title">
<span>{{ index + 1 }}</span>
</view>
<view class="remove" @click="removeUser(index)">
<text>报名人员信息</text>
<text class="form-subtitle">{{ activeTabIndex + 1 }}/{{ formData.userList.length }}</text>
<view class="user-actions">
<view class="remove-user" @click="removeUser(activeTabIndex)" v-if="formData.userList.length > 1">
<t-icon name="delete" size="18px"></t-icon>
</view>
</view>
</view>
<!-- 所有人员的表单,使用v-show控制显示 -->
<view v-for="(user, index) in formData.userList" :key="index" class="form-fields-container" v-show="activeTabIndex === index">
<t-form-item label="姓名" :name="`userList[${index}].userName`"
:rules="[{required:true,message: '请输入姓名'}]">
:rules="[{required:true,message: '请输入姓名'}]">
<t-input v-model="user.userName" borderless placeholder="请输入内容"></t-input>
</t-form-item>
<t-form-item label="性别" :name="`userList[${index}].sex`" :rules="[{required:true,message: '请选择性别'}]">
@@ -51,47 +87,65 @@
<t-radio :block="false" label="女" value="女"/>
</t-radio-group>
</t-form-item>
<t-form-item label="单位名称">
<t-input v-model="user.unitName" borderless placeholder="请输入单位名称" readonly></t-input>
</t-form-item>
<t-form-item label="职务" :name="`userList[${index}].position`"
:rules="[{required:true,message: '请输入职务'}]">
:rules="[{required:true,message: '请输入职务'}]">
<t-input v-model="user.position" borderless placeholder="请输入职务"></t-input>
</t-form-item>
<t-form-item label="身份证号" :name="`userList[${index}].idCard`"
:rules="[{required:true,message: '请输入身份证号码'},{idcard:true, message: '请输入正确的身份证号码'}]">
:rules="[{required:true,message: '请输入身份证号码'},{idcard:true, message: '请输入正确的身份证号码'}]">
<t-input v-model="user.idCard" borderless placeholder="请输入身份证号码"></t-input>
</t-form-item>
<t-form-item label="联系电话" :name="`userList[${index}].phone`"
:rules="[{required:true,message: '请输入联系电话'},{telnumber:true, message: '请输入正确的联系电话'}]">
:rules="[{required:true,message: '请输入联系电话'},{telnumber:true, message: '请输入正确的联系电话'}]">
<t-input v-model="user.phone" borderless placeholder="请输入联系电话"></t-input>
</t-form-item>
<t-form-item label="是否住宿" :name="`userList[${index}].isStay`"
:rules="[{required:true,message: '请选择是否住宿'}]">
:rules="[{required:true,message: '请选择是否住宿'}]">
<t-radio-group v-model="user.isStay" class="radio-box">
<t-radio :block="false" name="radio" :value="true" label="是"/>
<t-radio :block="false" name="radio" :value="false" label="否"/>
</t-radio-group>
</t-form-item>
<t-form-item label="是否联系人" :name="`userList[${index}].isContacts`"
:rules="[{required:true,message: '请选择是否为联系人'}]">
:rules="[{required:true,message: '请选择是否为联系人'}]">
<t-radio-group v-model="user.isContacts" class="radio-box">
<t-radio :block="false" name="radio" :value="true" label="是"/>
<t-radio :block="false" name="radio" :value="false" label="否"/>
</t-radio-group>
</t-form-item>
<t-form-item label="展示才艺" :name="`userList[${index}].isShowSkill`" :rules="[{required:true,message: '请选择是否展示才艺'}]">
<t-radio-group v-model="user.isShowSkill" class="radio-box">
<t-radio :block="false" name="radio" :value="true" label="是"/>
<t-radio :block="false" name="radio" :value="false" label="否"/>
</t-radio-group>
</t-form-item>
<template v-if="`userList[${index}].isShowSkill`">
<t-form-item label="才艺名称" :name="`userList[${index}].skillName`" :rules="[{required:user.isShowSkill===true,message: '请填写才艺名称'}]">
<t-input v-model="user.skillName" borderless placeholder="请输入才艺名称"></t-input>
</t-form-item>
<t-form-item label="所需器材" :name="`userList[${index}].equip`" :rules="[{required:user.isShowSkill===true,message: '请填写才艺所需器材'}]">
<t-input v-model="user.equip" borderless placeholder="请填写才艺所需器材"></t-input>
</t-form-item>
</template>
<t-form-item label="参加嘉年华" :name="`userList[${index}].isParty`" :rules="[{required:true,message: '请选择是否参加嘉年华'}]">
<t-radio-group v-model="user.isParty" class="radio-box">
<t-radio :block="false" name="radio" :value="true" label="是"/>
<t-radio :block="false" name="radio" :value="false" label="否"/>
</t-radio-group>
</t-form-item>
</view>
</view>
</t-form>
</view>
<!-- 悬浮添加按钮 -->
<view class="float-add-btn" @click="formData.userList.push({})">
<t-icon name="plus" size="24px" color="#fff"></t-icon>
</view>
<view class="footer">
<view class="agreement-checkbox">
<t-checkbox label="多选" icon="rectangle" v-model:checked="agreementChecked" default-checked>
<label>我已阅读并同意
<text class="agreement-link">《参赛承诺书》</text>
<text class="agreement-link" @click="showAgreementPopup">《参赛承诺书》</text>
</label>
</t-checkbox>
</view>
@@ -112,12 +166,32 @@
:steps="guideSteps"
>
</t-guide>
</view>
<!-- 协议确认弹窗 -->
<t-popup v-model:visible="agreementPopupVisible" placement="center" :close-on-overlay-click="false">
<view class="agreement-popup">
<view class="agreement-popup-header">
<text class="agreement-popup-title">参赛条款与免责声明</text>
</view>
<view class="agreement-popup-content">
<view class="agreement-popup-text">
<!-- 阅读并同意本承诺书内容,点击"确定并报名"即表示您已充分理解并接受所有条款,承担相应法律责任。-->
<rich-text :nodes="agreementContent"></rich-text>
</view>
</view>
<view class="agreement-popup-footer">
<t-button variant="outline" block @click="closeAgreementPopup" class="agreement-btn">取消</t-button>
<t-button theme="primary" block :disabled="!canConfirmAgreement" @click="confirmAgreement" class="agreement-btn">
{{ canConfirmAgreement ? '确定并报名' : `请阅读(${agreementCountdown}秒)` }}
</t-button>
</view>
</view>
</t-popup>
</template>
<script setup>
import {ref} from 'vue'
import {ref, computed} from 'vue'
import {useRequest} from '@/utils/request';
import {Dialog, Toast} from "tdesign-mobile-vue";
import {onLoad} from "@dcloudio/uni-app";
@@ -126,19 +200,84 @@ import {useUserStore} from "@/store";
const request = useRequest()
const userStore= useUserStore()
// 当前选中的选项卡索引
const activeTabIndex = ref(0)
// 切换选项卡
function switchTab(index) {
activeTabIndex.value = index
}
// 添加新用户
function addNewUser() {
formData.value.userList.push({
userName: '',
sex: null,
unitName: userStore?.userInfo?.unit?.name,
position: null,
idCard: null,
phone: null,
isStay: null,
isContacts: false,
isShowSkill: false,
skillName: null,
equip: null,
isParty: false
})
// 切换到新添加的用户选项卡
setTimeout(() => {
activeTabIndex.value = formData.value.userList.length - 1
// 滚动到最右边
scrollToRight()
}, 10)
}
// 滚动到指定选项卡
function scrollToTab(index) {
const query = uni.createSelectorQuery()
query.selectAll('.tab').boundingClientRect()
query.select('.tabs-scroll-view').boundingClientRect()
query.exec((res) => {
if (res && res[0] && res[0][index] && res[1]) {
const tabElement = res[0][index]
const scrollView = res[1]
const scrollLeft = tabElement.left - scrollView.left - (scrollView.width / 2) + (tabElement.width / 2)
uni.pageScrollTo({
scrollLeft: scrollLeft > 0 ? scrollLeft : 0,
duration: 300
})
}
})
}
// 滚动到最右边
function scrollToRight() {
const query = uni.createSelectorQuery()
query.select('.tabs-scroll-view').boundingClientRect()
query.exec((res) => {
if (res && res[0]) {
const scrollView = res[0]
const scrollWidth = scrollView.scrollWidth
const clientWidth = scrollView.width
const scrollLeft = scrollWidth - clientWidth
if (scrollLeft > 0) {
uni.pageScrollTo({
scrollLeft: scrollLeft,
duration: 300
})
}
}
})
}
// 引导部分
const guideCurrent = ref(-1)
const guideSteps = [
{
element:()=> document.querySelector('.float-add-btn'),
element:()=> document.querySelector('.header-add-btn'),
title: '添加活动报名人员',
placement: 'top'
},
{
element:()=> document.querySelector('.t-form'),
title: '填写报名信息',
placement: 'top'
},
{
element:()=> document.querySelector('.footer'),
title: '提交报名信息',
@@ -152,25 +291,52 @@ const isSignUp = ref(null)
const agreementChecked = ref(false)
const formRef = ref(null)
const formData = ref({
userList: [{}]
userList: []
})
// 协议弹窗相关
const agreementPopupVisible = ref(false)
const agreementCountdown = ref(5)
const countdownTimer = ref(null)
const canConfirmAgreement = ref(false)
const agreementContent = ref(null)
onLoad((option) => {
onLoad(async (option) => {
if (option.id) {
actId.value = option.id
formData.value.activityId = option.id
listSignUpUser()
getIsSignUp()
await listSignUpUser()
await getIsSignUp()
await getAgreement()
// 没报名进行引导
if(formData.value.userList.length === 0){
if(!formData.value.userList || formData.value.userList.length === 0){
guideCurrent.value = 0
//formData.value.unitName = userStore.userInfo?.unit.name
// 默认是本人的信息
formData.value.userList.push({
userName: userStore.userInfo.userName,
sex: userStore.userInfo.sex,
unitName: userStore?.userInfo?.unit?.name,
position: null,
idCard: userStore.userInfo.idCard,
phone: userStore.userInfo.phone,
isStay: null
})
}
} else {
Toast({message: 'ID不存在', theme: 'error'});
}
})
// 查询参赛承诺书
async function getAgreement() {
try {
const {code, data} = await request.get('/client/activity/agreement/' + actId.value)
if (code === 0 && data) {
agreementContent.value = data
}
}catch (e){
console.error(e)
}
}
// 查询报名人员信息
async function listSignUpUser() {
@@ -216,28 +382,86 @@ async function validaForm() {
// 承诺书
async function validateAgreement() {
return new Promise((resolve, reject) => {
if (agreementChecked.value) {
resolve(true)
return
}
// Always show the popup regardless of checkbox state
// if (agreementChecked.value) {
// resolve(true)
// return
// }
Dialog.confirm({
title: '提示',
content: '参赛条款与免责声明:阅读并同意本承诺书内容,点击"确定并报名"即表示您已充分理解并接受所有条款,承担相应法律责任。',
confirmBtn: '确定并报名',
cancelBtn: '取消',
onConfirm: () => {
agreementChecked.value = true
resolve(true)
},
onCancel: () => {
agreementChecked.value = false
resolve(false)
}
})
// 重置倒计时
agreementCountdown.value = 5
canConfirmAgreement.value = false
// 显示弹窗
agreementPopupVisible.value = true
// 启动倒计时
startAgreementCountdown(resolve)
})
}
// 启动协议倒计时
function startAgreementCountdown(resolvePromise) {
// 清除可能存在的旧计时器
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
}
// 设置新的计时器
countdownTimer.value = setInterval(() => {
agreementCountdown.value--
if (agreementCountdown.value <= 0) {
// 倒计时结束,允许确认
clearInterval(countdownTimer.value)
canConfirmAgreement.value = true
}
}, 1000)
// 保存 Promise 的 resolve 函数,以便在确认或取消时使用
window._resolveAgreementPromise = resolvePromise
}
// 确认协议
function confirmAgreement() {
// 只有在倒计时结束后才能确认
if (!canConfirmAgreement.value) return
// 关闭弹窗
agreementPopupVisible.value = false
// 清除计时器
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
}
// 标记已同意协议
agreementChecked.value = true
// 解析 Promise
if (window._resolveAgreementPromise) {
window._resolveAgreementPromise(true)
window._resolveAgreementPromise = null
}
}
// 关闭协议弹窗
function closeAgreementPopup() {
// 关闭弹窗
agreementPopupVisible.value = false
// 清除计时器
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
}
// 解析 Promise 为 false(未同意)
if (window._resolveAgreementPromise) {
window._resolveAgreementPromise(false)
window._resolveAgreementPromise = null
}
}
// 提交表单
async function onSubmit() {
const formRes = await validaForm()
@@ -287,12 +511,177 @@ async function removeUser(index) {
Toast('至少需要一个报名人员')
return
}
formData.value.userList.splice(index, 1)
Dialog.confirm({
title: '提示',
content: '确定要删除该报名人员吗?',
confirmBtn: '确定',
cancelBtn: '取消',
onConfirm: () => {
formData.value.userList.splice(index, 1)
// 如果删除的是当前选中的选项卡,则选中第一个选项卡
if (index === activeTabIndex.value) {
activeTabIndex.value = 0
}
// 如果删除的是前面的选项卡,则当前选中的选项卡索引减1
else if (index < activeTabIndex.value) {
activeTabIndex.value--
}
}
})
}
// 显示协议弹窗
function showAgreementPopup() {
// 重置倒计时
agreementCountdown.value = 5
canConfirmAgreement.value = false
// 显示弹窗
agreementPopupVisible.value = true
// 启动倒计时
startAgreementCountdown(() => {
// 这里不需要处理,因为协议弹窗会自动显示
})
}
</script>
<style scoped lang="less">
.tabs-container {
flex-shrink: 0;
background-color: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
position: relative;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.tabs-header {
padding: 10px 16px 5px;
}
.tabs-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 5px;
}
.title-text {
font-size: 16px;
font-weight: 500;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
}
.person-counter {
background-color: #f45a5a;
border-radius: 12px;
padding: 2px 10px;
margin-right: 8px;
}
.counter-text {
color: #fff;
font-size: 12px;
font-weight: 500;
}
.header-add-btn {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #f45a5a, #ff7676);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(244, 90, 90, 0.25);
transition: transform 0.2s ease;
}
.header-add-btn:active {
transform: scale(0.92);
}
.tabs-scroll-view {
width: 100%;
white-space: nowrap;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.tabs-scroll-view::-webkit-scrollbar {
display: none;
}
.tabs {
display: flex;
padding: 10px 12px;
align-items: center;
}
.tab {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 90px;
height: 35px;
padding: 0 14px;
margin: 0 5px;
font-size: 14px;
color: #666;
background-color: #f8f8f8;
border-radius: 19px;
transition: all 0.25s ease;
position: relative;
border: 1px solid transparent;
}
.tab-content {
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.tab-icon {
margin-right: 4px;
display: flex;
align-items: center;
}
.tab-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab.active {
color: #fff;
background-color: #f45a5a;
box-shadow: 0 3px 8px rgba(244, 90, 90, 0.25);
font-weight: 500;
}
.tabs-indicator {
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(244, 90, 90, 0.3), transparent);
}
.act-info {
position: fixed;
top: 44px;
@@ -340,6 +729,7 @@ async function removeUser(index) {
flex: 1;
overflow-y: auto;
margin-bottom: 130px;
padding-top: 10px;
}
.form-block {
@@ -347,6 +737,7 @@ async function removeUser(index) {
background: white;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
border-radius: 8px;
.form-block-title {
font-size: 16px;
@@ -355,83 +746,45 @@ async function removeUser(index) {
color: #333;
display: flex;
align-items: center;
justify-content: flex-start;
.t-icon {
margin-right: 5px;
color: var(--td-brand-color);
}
}
}
.user-card {
background: white;
margin-bottom: 20px;
position: relative;
border-left: 3px solid #e74c3c;
padding-left: 12px;
.user-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.user-card-header-title {
font-weight: 500;
color: #333;
font-size: 15px;
display: flex;
align-items: center;
span {
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
background-color: #e74c3c;
color: white;
border-radius: 50%;
font-size: 14px;
margin-right: 8px;
}
}
.remove {
color: #e74c3c;
//font-size: 14px;
display: flex;
align-items: center;
background: none;
border: none;
cursor: pointer;
.form-subtitle {
font-size: 12px;
color: #999;
margin-left: 8px;
font-weight: normal;
}
}
}
.float-add-btn {
position: fixed;
right: 32rpx;
bottom: 160rpx;
width: 110rpx;
height: 110rpx;
border-radius: 50%;
background-color: #e74c3c;
.form-fields-container {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0.6; }
to { opacity: 1; }
}
.user-actions {
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
transition: transform 0.2s;
margin-left: auto;
}
.float-add-btn:active {
transform: scale(0.95);
.remove-user {
padding: 5px;
color: #f45a5a;
cursor: pointer;
}
.radio-box {
width: 100%;
display: flex;
column-gap: 20px;
}
.footer {
@@ -439,12 +792,95 @@ async function removeUser(index) {
bottom: 0;
left: 0;
right: 0;
padding: 0 16px 16px;
background-color: white;
box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
z-index: 10;
max-width: 750px;
margin: 0 auto;
width: 100%;
padding: 16px;
background: white;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
z-index: 99;
}
.agreement-checkbox {
margin-bottom: 16px;
display: flex;
align-items: center;
font-size: 14px;
color: #666;
}
.agreement-link {
color: var(--td-brand-color);
}
.quick-nav {
position: fixed;
right: 16px;
bottom: 140px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 90;
}
.quick-nav-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(244, 90, 90, 0.9);
color: white;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: transform 0.2s ease, background-color 0.2s ease;
}
.quick-nav-btn:active {
transform: scale(0.95);
background-color: rgba(244, 90, 90, 1);
}
/* 协议弹窗样式 */
.agreement-popup {
width: 85vw;
max-width: 320px;
background-color: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.agreement-popup-header {
padding: 20px 16px 10px;
text-align: center;
border-bottom: 1px solid #f0f0f0;
}
.agreement-popup-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.agreement-popup-content {
padding: 20px 16px;
max-height: 60vh;
overflow-y: auto;
}
.agreement-popup-text {
font-size: 15px;
line-height: 1.5;
color: #666;
padding: 0 16px;
}
.agreement-popup-footer {
padding: 16px;
display: flex;
gap: 10px;
border-top: 1px solid #f0f0f0;
}
.agreement-btn {
height: 44px;
}
</style>
@@ -78,12 +78,7 @@ const navigateToEdit = () => {
<style scoped lang="less">
.page-container {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-bottom: 50px;
background-color: #fff;
min-height: 100vh;
}
/* 成功内容区域 */
+22 -2
View File
@@ -52,7 +52,7 @@
</div>
<view class="title-content">
<text class="main-title">第二届“钟声杯”直属工会趣味运动会</text>
<text class="main-title">第二届"钟声杯"直属工会趣味运动会</text>
<view class="sub-title-container">
<text class="sub-title">欢迎使用</text>
</view>
@@ -128,6 +128,13 @@
</view>
</view>
</view>
<!-- 底部信息 -->
<view class="footer-info">
<view class="footer-item">主办单位:浙江省教育工会</view>
<view class="footer-item">承办单位:杭州医学院工会</view>
<view class="footer-item">技术支持:武汉地大三思科技有限公司</view>
</view>
</view>
</template>
@@ -290,7 +297,7 @@ async function handleLogin() {
Toast({message: '登录成功', theme: 'success'})
const redirect = uni.getStorageSync("REDIRECT_URL")
if(redirect){
if(redirect.startsWith('/pages/index/index') || redirect.startsWith('/pages/profile/index')){
if(redirect.startsWith('/pages/index/index') || redirect.startsWith('/pages/activity/rank/index') || redirect.startsWith('/pages/profile/index')){
uni.switchTab({
url: redirect
})
@@ -515,4 +522,17 @@ body {
margin-left: 4px;
}
}
/* 底部信息样式 */
.footer-info {
padding: 20px 30px;
text-align: center;
background-color: #ffffff;
}
.footer-item {
font-size: 12px;
color: #999;
line-height: 1.8;
}
</style>
+21 -1
View File
@@ -53,7 +53,7 @@
</div>
<view class="title-content">
<text class="main-title">第二届“钟声杯”直属工会趣味运动会</text>
<text class="main-title">第二届"钟声杯"直属工会趣味运动会</text>
<view class="sub-title-container">
<text class="sub-title">用户注册</text>
</view>
@@ -134,6 +134,13 @@
</view>
</view>
<!-- 底部信息 -->
<view class="footer-info">
<view class="footer-item">主办单位:浙江省教育工会</view>
<view class="footer-item">承办单位:杭州医学院工会</view>
<view class="footer-item">技术支持:武汉地大三思科技有限公司</view>
</view>
<!-- 自定义单位选择弹窗 -->
<t-popup v-model="showUnitPicker" placement="bottom">
<view class="custom-picker">
@@ -687,4 +694,17 @@ onMounted(() => {
font-size: 28rpx;
color: #999;
}
/* 底部信息样式 */
.footer-info {
padding: 20px 30px;
text-align: center;
background-color: #ffffff;
}
.footer-item {
font-size: 12px;
color: #999;
line-height: 1.8;
}
</style>
+2 -1
View File
@@ -34,7 +34,8 @@ export default defineConfig({
},*/
proxy: {
'/api': {
target: 'http://119.91.224.39:9090/',
// target: 'http://119.91.224.39:9090/',
target: 'http://192.168.118.1:9090/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}