feat: 作品列表终分回算、评审进度详情展示对齐及评委管理优化
Made-with: Cursor
This commit is contained in:
parent
bc7c17b281
commit
180c22fe49
@ -18,6 +18,8 @@ import com.competition.modules.biz.contest.mapper.ContestMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestService;
|
||||
import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||
import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||
import com.competition.modules.sys.entity.SysTenant;
|
||||
import com.competition.modules.sys.mapper.SysTenantMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -39,6 +41,7 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
||||
private final ContestAttachmentMapper contestAttachmentMapper;
|
||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||
private final ContestWorkMapper contestWorkMapper;
|
||||
private final ContestWorkJudgeAssignmentMapper contestWorkJudgeAssignmentMapper;
|
||||
private final SysTenantMapper sysTenantMapper;
|
||||
|
||||
// 支持两种日期格式:ISO 格式 (T 分隔) 和空格分隔格式
|
||||
@ -160,6 +163,7 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
||||
|
||||
Map<Long, Long> registrationCountMap = new HashMap<>();
|
||||
Map<Long, Long> workCountMap = new HashMap<>();
|
||||
Map<Long, Long> reviewedWorkCountMap = new HashMap<>();
|
||||
if (!contestIds.isEmpty()) {
|
||||
// 报名数(所有状态)
|
||||
contestRegistrationMapper.selectList(
|
||||
@ -169,23 +173,42 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
||||
.collect(Collectors.groupingBy(BizContestRegistration::getContestId, Collectors.counting()))
|
||||
.forEach(registrationCountMap::put);
|
||||
|
||||
// 作品数(最新版本)
|
||||
contestWorkMapper.selectList(
|
||||
// 作品:最新有效版本;评审完成数 = 已分配且该作品全部分配记录均为 completed(与评委端、ProgressDetail 一致)
|
||||
List<BizContestWork> contestWorks = contestWorkMapper.selectList(
|
||||
new LambdaQueryWrapper<BizContestWork>()
|
||||
.in(BizContestWork::getContestId, contestIds)
|
||||
.eq(BizContestWork::getIsLatest, true))
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(BizContestWork::getContestId, Collectors.counting()))
|
||||
.forEach(workCountMap::put);
|
||||
.eq(BizContestWork::getIsLatest, true)
|
||||
.eq(BizContestWork::getValidState, 1));
|
||||
Set<Long> workIdSet = contestWorks.stream().map(BizContestWork::getId).collect(Collectors.toSet());
|
||||
Map<Long, List<BizContestWorkJudgeAssignment>> assignByWorkId = new HashMap<>();
|
||||
if (!workIdSet.isEmpty()) {
|
||||
List<BizContestWorkJudgeAssignment> allAssign = contestWorkJudgeAssignmentMapper.selectList(
|
||||
new LambdaQueryWrapper<BizContestWorkJudgeAssignment>()
|
||||
.in(BizContestWorkJudgeAssignment::getWorkId, workIdSet));
|
||||
assignByWorkId = allAssign.stream()
|
||||
.collect(Collectors.groupingBy(BizContestWorkJudgeAssignment::getWorkId));
|
||||
}
|
||||
for (BizContestWork w : contestWorks) {
|
||||
Long cid = w.getContestId();
|
||||
workCountMap.merge(cid, 1L, Long::sum);
|
||||
List<BizContestWorkJudgeAssignment> assigns = assignByWorkId.getOrDefault(w.getId(), Collections.emptyList());
|
||||
if (!assigns.isEmpty() && assigns.stream().allMatch(a -> "completed".equals(a.getStatus()))) {
|
||||
reviewedWorkCountMap.merge(cid, 1L, Long::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||
.map(entity -> {
|
||||
Map<String, Object> map = entityToMap(entity);
|
||||
Map<String, Object> countMap = new LinkedHashMap<>();
|
||||
long works = workCountMap.getOrDefault(entity.getId(), 0L);
|
||||
long reviewedWorks = reviewedWorkCountMap.getOrDefault(entity.getId(), 0L);
|
||||
countMap.put("registrations", registrationCountMap.getOrDefault(entity.getId(), 0L));
|
||||
countMap.put("works", workCountMap.getOrDefault(entity.getId(), 0L));
|
||||
countMap.put("works", works);
|
||||
map.put("_count", countMap);
|
||||
map.put("totalWorksCount", works);
|
||||
map.put("reviewedCount", reviewedWorks);
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@ -18,8 +18,15 @@ import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestWorkAttachmentMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestWorkService;
|
||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||
import com.competition.modules.biz.review.entity.BizContestReviewRule;
|
||||
import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||
import com.competition.modules.biz.review.entity.BizContestWorkScore;
|
||||
import com.competition.modules.biz.review.mapper.ContestJudgeMapper;
|
||||
import com.competition.modules.biz.review.mapper.ContestReviewRuleMapper;
|
||||
import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||
import com.competition.modules.biz.review.mapper.ContestWorkScoreMapper;
|
||||
import com.competition.modules.biz.review.util.ContestFinalScoreCalculator;
|
||||
import com.competition.modules.sys.entity.SysUser;
|
||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -28,6 +35,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@ -46,6 +54,9 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
||||
private final ContestMapper contestMapper;
|
||||
private final ContestWorkJudgeAssignmentMapper assignmentMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final ContestWorkScoreMapper contestWorkScoreMapper;
|
||||
private final ContestReviewRuleMapper contestReviewRuleMapper;
|
||||
private final ContestJudgeMapper contestJudgeMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@ -355,6 +366,49 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
||||
Map<Long, BizContest> finalContestMap = contestMap;
|
||||
Map<Long, List<BizContestWorkJudgeAssignment>> finalAssignmentMap = assignmentMap;
|
||||
Map<Long, String> finalJudgeNameMap = judgeNameMap;
|
||||
|
||||
// 批量评分:列表「评委评分」与详情抽屉一致;作品表 final_score 未落库时按评审规则从评分表回算
|
||||
Map<Long, List<BizContestWorkScore>> scoresByWorkId = new HashMap<>();
|
||||
if (!workIds.isEmpty()) {
|
||||
LambdaQueryWrapper<BizContestWorkScore> scoreWrapper = new LambdaQueryWrapper<>();
|
||||
scoreWrapper.in(BizContestWorkScore::getWorkId, workIds);
|
||||
scoreWrapper.eq(BizContestWorkScore::getValidState, 1);
|
||||
List<BizContestWorkScore> allPageScores = contestWorkScoreMapper.selectList(scoreWrapper);
|
||||
scoresByWorkId = allPageScores.stream()
|
||||
.collect(Collectors.groupingBy(BizContestWorkScore::getWorkId));
|
||||
}
|
||||
|
||||
Map<Long, String> contestCalculationRuleCache = new HashMap<>();
|
||||
Map<Long, Map<Long, BigDecimal>> contestWeightMapCache = new HashMap<>();
|
||||
for (Long cid : contestIds) {
|
||||
BizContest c = contestMap.get(cid);
|
||||
if (c == null) {
|
||||
continue;
|
||||
}
|
||||
String calculationRule = "average";
|
||||
if (c.getReviewRuleId() != null) {
|
||||
BizContestReviewRule rule = contestReviewRuleMapper.selectById(c.getReviewRuleId());
|
||||
if (rule != null && StringUtils.hasText(rule.getCalculationRule())) {
|
||||
calculationRule = rule.getCalculationRule();
|
||||
}
|
||||
}
|
||||
contestCalculationRuleCache.put(cid, calculationRule);
|
||||
|
||||
LambdaQueryWrapper<BizContestJudge> judgeWrapper = new LambdaQueryWrapper<>();
|
||||
judgeWrapper.eq(BizContestJudge::getContestId, cid);
|
||||
judgeWrapper.eq(BizContestJudge::getValidState, 1);
|
||||
List<BizContestJudge> judges = contestJudgeMapper.selectList(judgeWrapper);
|
||||
Map<Long, BigDecimal> weightMap = new HashMap<>();
|
||||
for (BizContestJudge j : judges) {
|
||||
weightMap.put(j.getJudgeId(), j.getWeight() != null ? j.getWeight() : BigDecimal.ONE);
|
||||
}
|
||||
contestWeightMapCache.put(cid, weightMap);
|
||||
}
|
||||
|
||||
Map<Long, List<BizContestWorkScore>> finalScoresByWorkId = scoresByWorkId;
|
||||
Map<Long, String> finalContestCalculationRuleCache = contestCalculationRuleCache;
|
||||
Map<Long, Map<Long, BigDecimal>> finalContestWeightMapCache = contestWeightMapCache;
|
||||
|
||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||
.map(work -> {
|
||||
Map<String, Object> map = workToMap(work);
|
||||
@ -413,6 +467,27 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
||||
countVo.put("assignments", workAssignments.size());
|
||||
map.put("_count", countVo);
|
||||
|
||||
int totalJudgesCount = workAssignments.size();
|
||||
long reviewedCount = workAssignments.stream()
|
||||
.filter(a -> "completed".equals(a.getStatus()))
|
||||
.count();
|
||||
map.put("totalJudgesCount", totalJudgesCount);
|
||||
map.put("reviewedCount", reviewedCount);
|
||||
|
||||
BigDecimal displayFinal = work.getFinalScore();
|
||||
if (displayFinal == null) {
|
||||
List<BizContestWorkScore> scores = finalScoresByWorkId.getOrDefault(work.getId(), Collections.emptyList());
|
||||
if (!scores.isEmpty()) {
|
||||
String rule = finalContestCalculationRuleCache.getOrDefault(work.getContestId(), "average");
|
||||
Map<Long, BigDecimal> wm = finalContestWeightMapCache.getOrDefault(work.getContestId(), Collections.emptyMap());
|
||||
displayFinal = ContestFinalScoreCalculator.compute(scores, rule, wm);
|
||||
}
|
||||
}
|
||||
if (displayFinal != null) {
|
||||
map.put("finalScore", displayFinal);
|
||||
map.put("averageScore", displayFinal);
|
||||
}
|
||||
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.competition.modules.biz.review.controller;
|
||||
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.modules.biz.review.dto.ContestJudgesForContestVo;
|
||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||
import com.competition.modules.biz.review.service.IContestJudgeService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
@ -10,7 +11,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "赛事评委")
|
||||
@ -35,8 +35,9 @@ public class ContestJudgeController {
|
||||
|
||||
@GetMapping("/contest/{contestId}")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "查询赛事评委列表")
|
||||
public Result<List<Map<String, Object>>> findByContest(@PathVariable Long contestId) {
|
||||
@Operation(summary = "查询赛事评委列表",
|
||||
description = "返回 assigned(显式关联)与 implicitPool(平台隐式池)。添加评委抽屉仅用 assigned 回显;作品分配可选池为 assigned ∪ implicitPool(前端合并)。")
|
||||
public Result<ContestJudgesForContestVo> findByContest(@PathVariable Long contestId) {
|
||||
return Result.success(contestJudgeService.findByContest(contestId));
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package com.competition.modules.biz.review.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 某赛事下的评委数据:显式关联与平台隐式池分离,避免扁平列表语义混淆。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "赛事评委查询结果")
|
||||
public class ContestJudgesForContestVo {
|
||||
|
||||
@Schema(description = "机构为该赛事显式添加的评委(t_biz_contest_judge),每条必有 id、judgeId;添加评委抽屉回显与提交差集仅基于此列表")
|
||||
private List<Map<String, Object>> assigned;
|
||||
|
||||
@Schema(description = "平台评委租户下对该赛事默认可用、未写入关联表的用户(id 为 null,isPlatform 为 true);可与 assigned 合并作为作品分配可选池")
|
||||
private List<Map<String, Object>> implicitPool;
|
||||
}
|
||||
@ -1,17 +1,21 @@
|
||||
package com.competition.modules.biz.review.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.modules.biz.review.dto.ContestJudgesForContestVo;
|
||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IContestJudgeService extends IService<BizContestJudge> {
|
||||
|
||||
BizContestJudge createJudge(Long contestId, Long judgeId, String specialty, BigDecimal weight, String description);
|
||||
|
||||
List<Map<String, Object>> findByContest(Long contestId);
|
||||
/**
|
||||
* 查询某赛事评委:{@link ContestJudgesForContestVo#getAssigned()} 为显式关联;
|
||||
* {@link ContestJudgesForContestVo#getImplicitPool()} 为平台默认可用未落库项。
|
||||
*/
|
||||
ContestJudgesForContestVo findByContest(Long contestId);
|
||||
|
||||
Map<String, Object> findDetail(Long id);
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.modules.biz.review.dto.ContestJudgesForContestVo;
|
||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||
import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||
import com.competition.modules.biz.review.mapper.ContestJudgeMapper;
|
||||
@ -57,7 +58,7 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> findByContest(Long contestId) {
|
||||
public ContestJudgesForContestVo findByContest(Long contestId) {
|
||||
log.info("查询赛事评委列表,赛事ID:{}", contestId);
|
||||
|
||||
// 获取平台评委租户 ID
|
||||
@ -111,7 +112,8 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
|
||||
assignedCountMap.put(judgeId, assignmentMapper.selectCount(assignWrapper));
|
||||
}
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
List<Map<String, Object>> assigned = new ArrayList<>();
|
||||
List<Map<String, Object>> implicitPool = new ArrayList<>();
|
||||
|
||||
// 构建已显式分配的评委数据
|
||||
for (BizContestJudge j : judges) {
|
||||
@ -136,10 +138,10 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
|
||||
} else {
|
||||
map.put("isPlatform", false);
|
||||
}
|
||||
result.add(map);
|
||||
assigned.add(map);
|
||||
}
|
||||
|
||||
// 追加未显式分配的平台评委
|
||||
// 未显式分配的平台评委(隐式池)
|
||||
for (SysUser platformJudge : platformJudges) {
|
||||
if (assignedJudgeIds.contains(platformJudge.getId())) {
|
||||
continue; // 已在显式分配列表中,跳过
|
||||
@ -159,10 +161,13 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
|
||||
map.put("status", platformJudge.getStatus());
|
||||
map.put("organization", platformJudge.getOrganization());
|
||||
map.put("isPlatform", true);
|
||||
result.add(map);
|
||||
implicitPool.add(map);
|
||||
}
|
||||
|
||||
return result;
|
||||
ContestJudgesForContestVo vo = new ContestJudgesForContestVo();
|
||||
vo.setAssigned(assigned);
|
||||
vo.setImplicitPool(implicitPool);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -313,28 +313,55 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
||||
public List<Map<String, Object>> getJudgeContests(Long judgeId) {
|
||||
log.info("查询评委关联赛事,评委ID:{}", judgeId);
|
||||
|
||||
LambdaQueryWrapper<BizContestJudge> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestJudge::getJudgeId, judgeId);
|
||||
wrapper.eq(BizContestJudge::getValidState, 1);
|
||||
Set<Long> contestIds = new LinkedHashSet<>();
|
||||
|
||||
List<BizContestJudge> judgeRecords = judgeMapper.selectList(wrapper);
|
||||
Set<Long> contestIds = judgeRecords.stream().map(BizContestJudge::getContestId).collect(Collectors.toSet());
|
||||
LambdaQueryWrapper<BizContestJudge> judgeQw = new LambdaQueryWrapper<>();
|
||||
judgeQw.eq(BizContestJudge::getJudgeId, judgeId);
|
||||
judgeQw.eq(BizContestJudge::getValidState, 1);
|
||||
for (BizContestJudge r : judgeMapper.selectList(judgeQw)) {
|
||||
contestIds.add(r.getContestId());
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignQw = new LambdaQueryWrapper<>();
|
||||
assignQw.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||
assignQw.select(BizContestWorkJudgeAssignment::getContestId);
|
||||
for (BizContestWorkJudgeAssignment a : assignmentMapper.selectList(assignQw)) {
|
||||
contestIds.add(a.getContestId());
|
||||
}
|
||||
|
||||
if (contestIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<BizContest> contests = contestMapper.selectBatchIds(contestIds);
|
||||
return contests.stream().map(c -> {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("contestId", c.getId());
|
||||
map.put("contestName", c.getContestName());
|
||||
map.put("contestState", c.getContestState());
|
||||
map.put("status", c.getStatus());
|
||||
map.put("reviewStartTime", c.getReviewStartTime());
|
||||
map.put("reviewEndTime", c.getReviewEndTime());
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
return contests.stream()
|
||||
.sorted(Comparator.comparing(BizContest::getId).reversed())
|
||||
.map(c -> {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("contestId", c.getId());
|
||||
map.put("contestName", c.getContestName());
|
||||
map.put("contestState", c.getContestState());
|
||||
map.put("status", c.getStatus());
|
||||
map.put("reviewStartTime", c.getReviewStartTime());
|
||||
map.put("reviewEndTime", c.getReviewEndTime());
|
||||
|
||||
LambdaQueryWrapper<BizContestWorkJudgeAssignment> totalW = new LambdaQueryWrapper<>();
|
||||
totalW.eq(BizContestWorkJudgeAssignment::getContestId, c.getId());
|
||||
totalW.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||
long totalAssigned = assignmentMapper.selectCount(totalW);
|
||||
|
||||
LambdaQueryWrapper<BizContestWorkJudgeAssignment> doneW = new LambdaQueryWrapper<>();
|
||||
doneW.eq(BizContestWorkJudgeAssignment::getContestId, c.getId());
|
||||
doneW.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||
doneW.eq(BizContestWorkJudgeAssignment::getStatus, "completed");
|
||||
long reviewed = assignmentMapper.selectCount(doneW);
|
||||
|
||||
map.put("totalAssigned", totalAssigned);
|
||||
map.put("reviewed", reviewed);
|
||||
map.put("pending", totalAssigned - reviewed);
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -348,7 +375,13 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
||||
wrapper.eq(BizContestWorkJudgeAssignment::getContestId, contestId);
|
||||
|
||||
if (StringUtils.hasText(reviewStatus)) {
|
||||
wrapper.eq(BizContestWorkJudgeAssignment::getStatus, reviewStatus);
|
||||
if ("reviewed".equalsIgnoreCase(reviewStatus)) {
|
||||
wrapper.eq(BizContestWorkJudgeAssignment::getStatus, "completed");
|
||||
} else if ("pending".equalsIgnoreCase(reviewStatus)) {
|
||||
wrapper.ne(BizContestWorkJudgeAssignment::getStatus, "completed");
|
||||
} else {
|
||||
wrapper.eq(BizContestWorkJudgeAssignment::getStatus, reviewStatus);
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.orderByAsc(BizContestWorkJudgeAssignment::getAssignmentTime);
|
||||
@ -675,13 +708,19 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
||||
public Map<String, Object> getJudgeContestDetail(Long judgeId, Long contestId) {
|
||||
log.info("获取评委赛事详情,评委ID:{},赛事ID:{}", judgeId, contestId);
|
||||
|
||||
// 验证评委属于该赛事
|
||||
// 显式评委 或 已有作品分配记录
|
||||
LambdaQueryWrapper<BizContestJudge> judgeWrapper = new LambdaQueryWrapper<>();
|
||||
judgeWrapper.eq(BizContestJudge::getContestId, contestId);
|
||||
judgeWrapper.eq(BizContestJudge::getJudgeId, judgeId);
|
||||
judgeWrapper.eq(BizContestJudge::getValidState, 1);
|
||||
if (judgeMapper.selectCount(judgeWrapper) == 0) {
|
||||
throw BusinessException.of(ErrorCode.FORBIDDEN, "您不是该赛事的评委");
|
||||
boolean explicitJudge = judgeMapper.selectCount(judgeWrapper) > 0;
|
||||
if (!explicitJudge) {
|
||||
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignCheck = new LambdaQueryWrapper<>();
|
||||
assignCheck.eq(BizContestWorkJudgeAssignment::getContestId, contestId);
|
||||
assignCheck.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||
if (assignmentMapper.selectCount(assignCheck) == 0) {
|
||||
throw BusinessException.of(ErrorCode.FORBIDDEN, "您不是该赛事的评委");
|
||||
}
|
||||
}
|
||||
|
||||
BizContest contest = contestMapper.selectById(contestId);
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
package com.competition.modules.biz.review.util;
|
||||
|
||||
import com.competition.modules.biz.review.entity.BizContestWorkScore;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 与 {@code ContestReviewServiceImpl#calculateFinalScore}、成果发布计算逻辑一致的终分计算(列表场景对不足条数规则做温和回退)。
|
||||
*/
|
||||
public final class ContestFinalScoreCalculator {
|
||||
|
||||
private ContestFinalScoreCalculator() {
|
||||
}
|
||||
|
||||
public static BigDecimal compute(
|
||||
List<BizContestWorkScore> scores,
|
||||
String calculationRule,
|
||||
Map<Long, BigDecimal> weightMap) {
|
||||
if (scores == null || scores.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String rule = calculationRule != null ? calculationRule : "average";
|
||||
Map<Long, BigDecimal> wm = weightMap != null ? weightMap : Collections.emptyMap();
|
||||
|
||||
List<BigDecimal> scoreValues = scores.stream()
|
||||
.map(BizContestWorkScore::getTotalScore)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
switch (rule) {
|
||||
case "max":
|
||||
return Collections.max(scoreValues);
|
||||
case "min":
|
||||
return Collections.min(scoreValues);
|
||||
case "weighted":
|
||||
BigDecimal weightedSum = BigDecimal.ZERO;
|
||||
BigDecimal totalWeight = BigDecimal.ZERO;
|
||||
for (BizContestWorkScore s : scores) {
|
||||
BigDecimal w = wm.getOrDefault(s.getJudgeId(), BigDecimal.ONE);
|
||||
weightedSum = weightedSum.add(s.getTotalScore().multiply(w));
|
||||
totalWeight = totalWeight.add(w);
|
||||
}
|
||||
return totalWeight.compareTo(BigDecimal.ZERO) > 0
|
||||
? weightedSum.divide(totalWeight, 2, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
case "remove_max_min":
|
||||
if (scoreValues.size() < 3) {
|
||||
BigDecimal sum = scoreValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return sum.divide(BigDecimal.valueOf(scoreValues.size()), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
List<BigDecimal> trimmed = scoreValues.subList(1, scoreValues.size() - 1);
|
||||
BigDecimal trimmedSum = trimmed.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return trimmedSum.divide(BigDecimal.valueOf(trimmed.size()), 2, RoundingMode.HALF_UP);
|
||||
case "remove_min":
|
||||
if (scoreValues.size() < 2) {
|
||||
return scoreValues.get(0);
|
||||
}
|
||||
List<BigDecimal> withoutMin = scoreValues.subList(1, scoreValues.size());
|
||||
BigDecimal withoutMinSum = withoutMin.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return withoutMinSum.divide(BigDecimal.valueOf(withoutMin.size()), 2, RoundingMode.HALF_UP);
|
||||
case "average":
|
||||
default:
|
||||
BigDecimal sum = scoreValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return sum.divide(BigDecimal.valueOf(scoreValues.size()), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,4 +31,6 @@
|
||||
|
||||
## 评委端
|
||||
|
||||
(暂无)
|
||||
| 文档 | 模块 | 状态 | 日期 |
|
||||
|------|------|------|------|
|
||||
| [评审任务](./judge-portal/review-tasks.md) | 评审任务 / 作品列表 | 已实现 | 2026-04-08 |
|
||||
|
||||
60
docs/design/judge-portal/review-tasks.md
Normal file
60
docs/design/judge-portal/review-tasks.md
Normal file
@ -0,0 +1,60 @@
|
||||
# 评委端:评审任务
|
||||
|
||||
> 所属端:评委端(`tenant_id` 对应评委租户,如 `code=judge`)
|
||||
> 菜单与定位见 [菜单配置说明](../menu-config.md) 中「评委端」章节:仅能查看**被分配**的活动与作品。
|
||||
|
||||
## 活动列表
|
||||
|
||||
**接口**:`GET /contests/reviews/judge/contests`
|
||||
|
||||
**活动来源(并集)**:
|
||||
|
||||
1. `t_biz_contest_judge` 中 `judge_id` 且 `valid_state=1` 的赛事(机构显式添加的评委);
|
||||
2. `t_biz_contest_work_judge_assignment` 中该评委出现过的 `contest_id`(含仅通过作品分配参与的隐式场景)。
|
||||
|
||||
**响应字段(与前端 `activities/Review.vue` 对齐)**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `contestId` | 活动 ID |
|
||||
| `contestName` | 活动名称 |
|
||||
| `contestState` / `status` | 活动状态 |
|
||||
| `reviewStartTime` / `reviewEndTime` | 评审时间窗 |
|
||||
| `totalAssigned` | 该评委在该活动下的分配记录总数 |
|
||||
| `reviewed` | 其中 `status=completed` 的数量(已提交评分) |
|
||||
| `pending` | `totalAssigned - reviewed`(待评审) |
|
||||
|
||||
## 活动下作品列表
|
||||
|
||||
**接口**:`GET /contests/reviews/judge/contests/{contestId}/works`
|
||||
|
||||
**分配状态 `reviewStatus` 查询参数(与库表兼容)**:
|
||||
|
||||
- 库中 `t_biz_contest_work_judge_assignment.status` 实际使用:`assigned`(已分配未评完)、`completed`(已评审)。
|
||||
- 前端下拉「未评审」传 `pending` → 后端按 **`status != completed`** 筛选。
|
||||
- 前端「已评审」传 `reviewed` → 后端按 **`status = completed`** 筛选。
|
||||
|
||||
**作品编号**:`workNo` 为空时,前端可用作品 `workId` 展示兜底(如 `#123`)。
|
||||
|
||||
## 活动详情(含评审规则)
|
||||
|
||||
**接口**:`GET /contests/reviews/judge/contests/{contestId}/detail`
|
||||
|
||||
**权限**:满足以下**任一**即可:
|
||||
|
||||
- 存在有效的 `t_biz_contest_judge` 关联;或
|
||||
- 存在该 `contestId` + `judgeId` 的作品分配记录。
|
||||
|
||||
避免「列表能进、详情 403」与隐式评委场景不一致。
|
||||
|
||||
---
|
||||
|
||||
## 与租户端「评审进度」的口径对齐
|
||||
|
||||
| 维度 | 租户机构端 `contests/reviews/progress`(活动列表行) | 评委端 `activities/review`(上表) |
|
||||
|------|------------------------------------------------------|-------------------------------------|
|
||||
| 数据来源 | `GET /contests` 列表项中的 `reviewedCount` / `totalWorksCount` | `GET /contests/reviews/judge/contests` |
|
||||
| 含义 | `totalWorksCount`:该活动最新有效作品总数。`reviewedCount`:**该活动下已分配评委且全部分配记录均为 `completed` 的作品数**(与分配表 `t_biz_contest_work_judge_assignment` 一致,与作品表 `accepted`/`awarded` 终态无关) | **评委维度**:`reviewed`/`totalAssigned`/`pending` 为该评委在分配表上的任务数;与租户「整作品是否全部评委评完」为不同聚合粒度 |
|
||||
| 作品列表/详情 | `GET /contests/works` 每条作品含 `reviewedCount`/`totalJudgesCount`(按该作品分配条数统计) | 评委在单活动下作品列表同样基于分配 `completed` |
|
||||
|
||||
说明:顶部「作品统计」卡片若仍按作品 `status` 汇总,可能与逐活动行「分配完成作品数」不完全同数,属汇总维度不同;列表/详情/评委任务以分配表为准。
|
||||
@ -132,6 +132,8 @@
|
||||
|
||||
**定位**:评委评审工作台,只能看到自己被分配的活动和作品。
|
||||
|
||||
**详细接口与字段说明**:[评委端评审任务](./judge-portal/review-tasks.md)。
|
||||
|
||||
**一级菜单**:1 个(我的评审)
|
||||
|
||||
```
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
> 所属端:租户端(机构管理员视角)
|
||||
> 状态:已优化
|
||||
> 创建日期:2026-03-31
|
||||
> 最后更新:2026-03-31
|
||||
> 最后更新:2026-04-08
|
||||
|
||||
---
|
||||
|
||||
@ -58,6 +58,12 @@
|
||||
- [x] 主色调统一 #6366f1
|
||||
- [x] 冻结/解冻二次确认
|
||||
|
||||
#### 赛事评委接口(`GET /contests/judges/contest/:id`)
|
||||
|
||||
- 响应为结构化对象,包含两部分:**`assigned`**(机构在该赛事下**显式添加**的评委,对应 `t_biz_contest_judge`,每条均有 `id`、`judgeId` 等)与 **`implicitPool`**(平台评委租户下对该赛事**默认可用**、尚未写入关联表的用户,`id` 为 null,`isPlatform` 为 true)。
|
||||
- **添加评委抽屉**:「已选」回显与提交时的增删差集**仅基于 `assigned`**;可选评委仍来自评委管理分页接口。
|
||||
- **作品分配**:可选评委池为 **`assigned` ∪ `implicitPool`**(前端合并);表格行键与选中状态统一使用 **`judgeId`**,与分配接口提交的 `judgeIds` 一致。
|
||||
|
||||
### 报名管理(Index)
|
||||
- [x] 去掉个人/团队 Tab,合并展示加类型列
|
||||
- [x] 统计概览(总报名/待审核/已通过/已拒绝)
|
||||
@ -93,6 +99,7 @@
|
||||
- [x] 评审状态改用实际完成率(无作品/未开始/进行中/已完成)
|
||||
- [x] 进度数字颜色区分
|
||||
- [x] 评审进度详情页筛选修复(评审进度前端过滤生效)
|
||||
- 活动列表接口 `GET /contests` 为每行返回 `reviewedCount`(该活动下**已分配且全部分配均为 completed** 的作品数)与 `totalWorksCount`(最新有效作品总数),与分配表及评委端评审任务口径一致;见 [评委端评审任务](../judge-portal/review-tasks.md#与租户端评审进度的口径对齐)。
|
||||
|
||||
### 评审规则
|
||||
- [x] 组件映射修复
|
||||
|
||||
@ -66,6 +66,10 @@ export interface Contest {
|
||||
teams: number;
|
||||
judges: number;
|
||||
};
|
||||
/** 评审进度(列表接口 findAll 填充):已分配且全部分配均为 completed 的作品数(与分配表一致) */
|
||||
reviewedCount?: number;
|
||||
/** 评审进度(列表接口 findAll 填充):最新有效作品总数,与 _count.works 一致 */
|
||||
totalWorksCount?: number;
|
||||
}
|
||||
|
||||
export interface CreateContestForm {
|
||||
@ -119,7 +123,12 @@ export interface QueryContestParams extends PaginationParams {
|
||||
status?: "ongoing" | "finished";
|
||||
contestType?: string;
|
||||
visibility?: string;
|
||||
stage?: "unpublished" | "registering" | "submitting" | "reviewing" | "finished";
|
||||
stage?:
|
||||
| "unpublished"
|
||||
| "registering"
|
||||
| "submitting"
|
||||
| "reviewing"
|
||||
| "finished";
|
||||
creatorTenantId?: number;
|
||||
role?: "student" | "teacher" | "judge";
|
||||
}
|
||||
@ -383,6 +392,8 @@ export interface ContestWork {
|
||||
// 评审统计字段(由后端计算返回)
|
||||
reviewedCount?: number;
|
||||
totalJudgesCount?: number;
|
||||
/** 终分(作品列表 workToMap 等接口返回) */
|
||||
finalScore?: number | null;
|
||||
averageScore?: number | null;
|
||||
}
|
||||
|
||||
@ -459,6 +470,8 @@ export interface ContestWorkJudgeAssignment {
|
||||
}
|
||||
|
||||
export interface ContestWorkScore {
|
||||
/** 与 id 同源,评分列表扁平接口常用 scoreId */
|
||||
scoreId?: number;
|
||||
id: number;
|
||||
tenantId: number;
|
||||
contestId: number;
|
||||
@ -523,7 +536,8 @@ export interface CreateNoticeForm {
|
||||
|
||||
// ==================== 评委相关类型 ====================
|
||||
export interface ContestJudge {
|
||||
id: number;
|
||||
/** t_biz_contest_judge 主键;implicitPool 中隐式平台评委为 null */
|
||||
id?: number | null;
|
||||
contestId: number;
|
||||
judgeId: number;
|
||||
specialty?: string;
|
||||
@ -534,6 +548,14 @@ export interface ContestJudge {
|
||||
createTime?: string;
|
||||
modifyTime?: string;
|
||||
validState?: number;
|
||||
/** 隐式平台评委(未写入关联表时由后端追加) */
|
||||
isPlatform?: boolean;
|
||||
judgeName?: string;
|
||||
judgeUsername?: string;
|
||||
assignedCount?: number;
|
||||
organization?: string;
|
||||
status?: string;
|
||||
tenantId?: number;
|
||||
contest?: Contest;
|
||||
judge?: {
|
||||
id: number;
|
||||
@ -541,8 +563,8 @@ export interface ContestJudge {
|
||||
nickname: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
gender?: 'male' | 'female';
|
||||
status?: 'enabled' | 'disabled';
|
||||
gender?: "male" | "female";
|
||||
status?: "enabled" | "disabled";
|
||||
tenantId?: number;
|
||||
tenant?: {
|
||||
id: number;
|
||||
@ -562,6 +584,19 @@ export interface ContestJudge {
|
||||
};
|
||||
}
|
||||
|
||||
/** GET /contests/judges/contest/:id 结构化响应:显式关联与隐式平台池分离 */
|
||||
export interface ContestJudgesForContestResponse {
|
||||
assigned: ContestJudge[];
|
||||
implicitPool: ContestJudge[];
|
||||
}
|
||||
|
||||
/** 作品分配等场景:合并为可选评委池(assigned ∪ implicitPool) */
|
||||
export function flattenContestJudgePool(
|
||||
r: ContestJudgesForContestResponse,
|
||||
): ContestJudge[] {
|
||||
return [...(r.assigned ?? []), ...(r.implicitPool ?? [])];
|
||||
}
|
||||
|
||||
export interface CreateJudgeForm {
|
||||
contestId: number;
|
||||
judgeId: number;
|
||||
@ -582,22 +617,22 @@ export const contestsApi = {
|
||||
|
||||
// 获取活动列表
|
||||
getList: async (
|
||||
params: QueryContestParams
|
||||
params: QueryContestParams,
|
||||
): Promise<PaginationResponse<Contest>> => {
|
||||
const response = await request.get<any, PaginationResponse<Contest>>(
|
||||
"/contests",
|
||||
{ params }
|
||||
{ params },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 获取我参与的活动列表
|
||||
getMyContests: async (
|
||||
params: QueryContestParams
|
||||
params: QueryContestParams,
|
||||
): Promise<PaginationResponse<Contest>> => {
|
||||
const response = await request.get<any, PaginationResponse<Contest>>(
|
||||
"/contests/my-contests",
|
||||
{ params }
|
||||
{ params },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -623,11 +658,11 @@ export const contestsApi = {
|
||||
// 发布/撤回活动
|
||||
publish: async (
|
||||
id: number,
|
||||
contestState: "unpublished" | "published"
|
||||
contestState: "unpublished" | "published",
|
||||
): Promise<Contest> => {
|
||||
const response = await request.patch<any, Contest>(
|
||||
`/contests/${id}/publish`,
|
||||
{ contestState }
|
||||
{ contestState },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -639,13 +674,17 @@ export const contestsApi = {
|
||||
|
||||
// 标记活动完结
|
||||
finish: async (id: number): Promise<Contest> => {
|
||||
const response = await request.patch<any, Contest>(`/contests/${id}/finish`);
|
||||
const response = await request.patch<any, Contest>(
|
||||
`/contests/${id}/finish`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 重新开启已完结的活动
|
||||
reopen: async (id: number): Promise<Contest> => {
|
||||
const response = await request.patch<any, Contest>(`/contests/${id}/reopen`);
|
||||
const response = await request.patch<any, Contest>(
|
||||
`/contests/${id}/reopen`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
};
|
||||
@ -655,7 +694,7 @@ export const attachmentsApi = {
|
||||
// 获取活动附件列表
|
||||
getList: async (contestId: number): Promise<ContestAttachment[]> => {
|
||||
const response = await request.get<any, ContestAttachment[]>(
|
||||
`/contests/attachments/contest/${contestId}`
|
||||
`/contests/attachments/contest/${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -664,7 +703,7 @@ export const attachmentsApi = {
|
||||
create: async (data: CreateAttachmentForm): Promise<ContestAttachment> => {
|
||||
const response = await request.post<any, ContestAttachment>(
|
||||
"/contests/attachments",
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -710,25 +749,30 @@ export interface QueryReviewRuleParams {
|
||||
|
||||
export const reviewRulesApi = {
|
||||
// 获取评审规则列表
|
||||
getList: async (params?: QueryReviewRuleParams): Promise<{
|
||||
getList: async (
|
||||
params?: QueryReviewRuleParams,
|
||||
): Promise<{
|
||||
list: ReviewRule[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}> => {
|
||||
const response = await request.get<any, {
|
||||
list: ReviewRule[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}>("/contests/review-rules", { params });
|
||||
const response = await request.get<
|
||||
any,
|
||||
{
|
||||
list: ReviewRule[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
>("/contests/review-rules", { params });
|
||||
return response;
|
||||
},
|
||||
|
||||
// 获取所有可用的评审规则(用于活动创建时选择)
|
||||
getForSelect: async (): Promise<ReviewRuleForSelect[]> => {
|
||||
const response = await request.get<any, ReviewRuleForSelect[]>(
|
||||
"/contests/review-rules/select"
|
||||
"/contests/review-rules/select",
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -736,7 +780,7 @@ export const reviewRulesApi = {
|
||||
// 获取评审规则详情
|
||||
getDetail: async (id: number): Promise<ReviewRule> => {
|
||||
const response = await request.get<any, ReviewRule>(
|
||||
`/contests/review-rules/${id}`
|
||||
`/contests/review-rules/${id}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -745,7 +789,7 @@ export const reviewRulesApi = {
|
||||
create: async (data: CreateReviewRuleForm): Promise<ReviewRule> => {
|
||||
const response = await request.post<any, ReviewRule>(
|
||||
"/contests/review-rules",
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -753,11 +797,11 @@ export const reviewRulesApi = {
|
||||
// 更新评审规则
|
||||
update: async (
|
||||
id: number,
|
||||
data: Partial<CreateReviewRuleForm>
|
||||
data: Partial<CreateReviewRuleForm>,
|
||||
): Promise<ReviewRule> => {
|
||||
const response = await request.patch<any, ReviewRule>(
|
||||
`/contests/review-rules/${id}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -776,14 +820,14 @@ export const registrationsApi = {
|
||||
if (contestId) params.contestId = contestId;
|
||||
const response = await request.get<any, RegistrationStats>(
|
||||
"/contests/registrations/stats",
|
||||
{ params }
|
||||
{ params },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 获取报名列表
|
||||
getList: async (
|
||||
params: QueryRegistrationParams
|
||||
params: QueryRegistrationParams,
|
||||
): Promise<PaginationResponse<ContestRegistration>> => {
|
||||
const response = await request.get<
|
||||
any,
|
||||
@ -795,28 +839,28 @@ export const registrationsApi = {
|
||||
// 获取报名详情
|
||||
getDetail: async (id: number): Promise<ContestRegistration> => {
|
||||
const response = await request.get<any, ContestRegistration>(
|
||||
`/contests/registrations/${id}`
|
||||
`/contests/registrations/${id}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 获取当前用户在某活动中的报名记录(包括作为团队成员的情况)
|
||||
getMyRegistration: async (
|
||||
contestId: number
|
||||
contestId: number,
|
||||
): Promise<ContestRegistration | null> => {
|
||||
const response = await request.get<any, ContestRegistration | null>(
|
||||
`/contests/registrations/my/${contestId}`
|
||||
`/contests/registrations/my/${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 创建报名
|
||||
create: async (
|
||||
data: CreateRegistrationForm
|
||||
data: CreateRegistrationForm,
|
||||
): Promise<ContestRegistration> => {
|
||||
const response = await request.post<any, ContestRegistration>(
|
||||
"/contests/registrations",
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -824,11 +868,11 @@ export const registrationsApi = {
|
||||
// 添加指导老师
|
||||
addTeacher: async (
|
||||
registrationId: number,
|
||||
teacherUserId: number
|
||||
teacherUserId: number,
|
||||
): Promise<any> => {
|
||||
const response = await request.post<any, any>(
|
||||
`/contests/registrations/${registrationId}/teachers`,
|
||||
{ teacherUserId }
|
||||
{ teacherUserId },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -836,33 +880,42 @@ export const registrationsApi = {
|
||||
// 移除指导老师
|
||||
removeTeacher: async (
|
||||
registrationId: number,
|
||||
teacherUserId: number
|
||||
teacherUserId: number,
|
||||
): Promise<void> => {
|
||||
await request.delete(
|
||||
`/contests/registrations/${registrationId}/teachers/${teacherUserId}`
|
||||
`/contests/registrations/${registrationId}/teachers/${teacherUserId}`,
|
||||
);
|
||||
},
|
||||
|
||||
// 审核报名
|
||||
review: async (
|
||||
id: number,
|
||||
data: ReviewRegistrationForm
|
||||
data: ReviewRegistrationForm,
|
||||
): Promise<ContestRegistration> => {
|
||||
const response = await request.patch<any, ContestRegistration>(
|
||||
`/contests/registrations/${id}/review`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 撤销报名审核
|
||||
revokeReview: async (id: number): Promise<ContestRegistration> => {
|
||||
return await request.patch<any, ContestRegistration>(`/contests/registrations/${id}/revoke`);
|
||||
return await request.patch<any, ContestRegistration>(
|
||||
`/contests/registrations/${id}/revoke`,
|
||||
);
|
||||
},
|
||||
|
||||
// 批量审核报名
|
||||
batchReview: async (data: { ids: number[]; registrationState: string; reason?: string }): Promise<{ success: boolean; count: number }> => {
|
||||
return await request.post<any, { success: boolean; count: number }>('/contests/registrations/batch-review', data);
|
||||
batchReview: async (data: {
|
||||
ids: number[];
|
||||
registrationState: string;
|
||||
reason?: string;
|
||||
}): Promise<{ success: boolean; count: number }> => {
|
||||
return await request.post<any, { success: boolean; count: number }>(
|
||||
"/contests/registrations/batch-review",
|
||||
data,
|
||||
);
|
||||
},
|
||||
|
||||
// 删除报名
|
||||
@ -876,7 +929,7 @@ export const teamsApi = {
|
||||
// 获取团队列表
|
||||
getList: async (contestId: number): Promise<ContestTeam[]> => {
|
||||
const response = await request.get<any, ContestTeam[]>(
|
||||
`/contests/teams/contest/${contestId}`
|
||||
`/contests/teams/contest/${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -884,7 +937,7 @@ export const teamsApi = {
|
||||
// 获取团队详情
|
||||
getDetail: async (id: number): Promise<ContestTeam> => {
|
||||
const response = await request.get<any, ContestTeam>(
|
||||
`/contests/teams/${id}`
|
||||
`/contests/teams/${id}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -893,7 +946,7 @@ export const teamsApi = {
|
||||
create: async (data: CreateTeamForm): Promise<ContestTeam> => {
|
||||
const response = await request.post<any, ContestTeam>(
|
||||
"/contests/teams",
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -901,11 +954,11 @@ export const teamsApi = {
|
||||
// 更新团队
|
||||
update: async (
|
||||
id: number,
|
||||
data: Partial<CreateTeamForm>
|
||||
data: Partial<CreateTeamForm>,
|
||||
): Promise<ContestTeam> => {
|
||||
const response = await request.patch<any, ContestTeam>(
|
||||
`/contests/teams/${id}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -913,11 +966,11 @@ export const teamsApi = {
|
||||
// 邀请成员
|
||||
inviteMember: async (
|
||||
teamId: number,
|
||||
data: InviteMemberForm
|
||||
data: InviteMemberForm,
|
||||
): Promise<ContestTeamMember> => {
|
||||
const response = await request.post<any, ContestTeamMember>(
|
||||
`/contests/teams/${teamId}/members`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -925,7 +978,7 @@ export const teamsApi = {
|
||||
// 移除成员
|
||||
removeMember: async (teamId: number, userId: number): Promise<void> => {
|
||||
return await request.delete<any, void>(
|
||||
`/contests/teams/${teamId}/members/${userId}`
|
||||
`/contests/teams/${teamId}/members/${userId}`,
|
||||
);
|
||||
},
|
||||
|
||||
@ -956,29 +1009,29 @@ export const worksApi = {
|
||||
if (contestId) params.contestId = contestId;
|
||||
const response = await request.get<any, WorksStats>(
|
||||
"/contests/works/stats",
|
||||
{ params }
|
||||
{ params },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 获取作品列表
|
||||
getList: async (
|
||||
params: QueryWorkParams
|
||||
params: QueryWorkParams,
|
||||
): Promise<PaginationResponse<ContestWork>> => {
|
||||
const response = await request.get<any, PaginationResponse<ContestWork>>(
|
||||
"/contests/works",
|
||||
{ params }
|
||||
{ params },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 获取教师指导的作品列表
|
||||
getGuidedWorks: async (
|
||||
params: QueryGuidedWorkParams
|
||||
params: QueryGuidedWorkParams,
|
||||
): Promise<PaginationResponse<GuidedWork>> => {
|
||||
const response = await request.get<any, PaginationResponse<GuidedWork>>(
|
||||
"/contests/works/guided",
|
||||
{ params }
|
||||
{ params },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -986,7 +1039,7 @@ export const worksApi = {
|
||||
// 获取作品详情
|
||||
getDetail: async (id: number): Promise<ContestWork> => {
|
||||
const response = await request.get<any, ContestWork>(
|
||||
`/contests/works/${id}`
|
||||
`/contests/works/${id}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -995,7 +1048,7 @@ export const worksApi = {
|
||||
submit: async (data: SubmitWorkForm): Promise<ContestWork> => {
|
||||
const response = await request.post<any, ContestWork>(
|
||||
"/contests/works/submit",
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1003,7 +1056,7 @@ export const worksApi = {
|
||||
// 获取作品版本列表
|
||||
getVersions: async (registrationId: number): Promise<ContestWork[]> => {
|
||||
const response = await request.get<any, ContestWork[]>(
|
||||
`/contests/works/registration/${registrationId}/versions`
|
||||
`/contests/works/registration/${registrationId}/versions`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1107,11 +1160,11 @@ export const reviewsApi = {
|
||||
// 分配作品给评委
|
||||
assignWork: async (
|
||||
contestId: number,
|
||||
data: AssignWorkForm
|
||||
data: AssignWorkForm,
|
||||
): Promise<ContestWorkJudgeAssignment[]> => {
|
||||
const response = await request.post<any, ContestWorkJudgeAssignment[]>(
|
||||
`/contests/reviews/assign?contestId=${contestId}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1119,11 +1172,11 @@ export const reviewsApi = {
|
||||
// 批量分配作品给评委
|
||||
batchAssignWorks: async (
|
||||
contestId: number,
|
||||
data: BatchAssignForm
|
||||
data: BatchAssignForm,
|
||||
): Promise<BatchAssignResult> => {
|
||||
const response = await request.post<any, BatchAssignResult>(
|
||||
`/contests/reviews/batch-assign?contestId=${contestId}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1131,7 +1184,7 @@ export const reviewsApi = {
|
||||
// 自动分配作品给评委
|
||||
autoAssignWorks: async (contestId: number): Promise<AutoAssignResult> => {
|
||||
const response = await request.post<any, AutoAssignResult>(
|
||||
`/contests/reviews/auto-assign?contestId=${contestId}`
|
||||
`/contests/reviews/auto-assign?contestId=${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1140,7 +1193,7 @@ export const reviewsApi = {
|
||||
score: async (data: CreateScoreForm): Promise<ContestWorkScore> => {
|
||||
const response = await request.post<any, ContestWorkScore>(
|
||||
"/contests/reviews/score",
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1148,21 +1201,21 @@ export const reviewsApi = {
|
||||
// 更新评分
|
||||
updateScore: async (
|
||||
scoreId: number,
|
||||
data: Partial<CreateScoreForm>
|
||||
data: Partial<CreateScoreForm>,
|
||||
): Promise<ContestWorkScore> => {
|
||||
const response = await request.patch<any, ContestWorkScore>(
|
||||
`/contests/reviews/score/${scoreId}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 获取分配给当前评委的作品
|
||||
getAssignedWorks: async (
|
||||
contestId: number
|
||||
contestId: number,
|
||||
): Promise<ContestWorkJudgeAssignment[]> => {
|
||||
const response = await request.get<any, ContestWorkJudgeAssignment[]>(
|
||||
`/contests/reviews/assigned?contestId=${contestId}`
|
||||
`/contests/reviews/assigned?contestId=${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1170,7 +1223,7 @@ export const reviewsApi = {
|
||||
// 获取评审进度统计
|
||||
getReviewProgress: async (contestId: number): Promise<ReviewProgress> => {
|
||||
const response = await request.get<any, ReviewProgress>(
|
||||
`/contests/reviews/progress/${contestId}`
|
||||
`/contests/reviews/progress/${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1178,7 +1231,7 @@ export const reviewsApi = {
|
||||
// 获取作品状态统计
|
||||
getWorkStatusStats: async (contestId: number): Promise<WorkStatusStats> => {
|
||||
const response = await request.get<any, WorkStatusStats>(
|
||||
`/contests/reviews/work-status/${contestId}`
|
||||
`/contests/reviews/work-status/${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1186,14 +1239,14 @@ export const reviewsApi = {
|
||||
// 获取作品评分列表
|
||||
getWorkScores: async (workId: number): Promise<ContestWorkScore[]> => {
|
||||
const response = await request.get<any, ContestWorkScore[]>(
|
||||
`/contests/reviews/work/${workId}/scores`
|
||||
`/contests/reviews/work/${workId}/scores`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 计算最终得分
|
||||
calculateFinalScore: async (
|
||||
workId: number
|
||||
workId: number,
|
||||
): Promise<{
|
||||
finalScore: number;
|
||||
scoreCount: number;
|
||||
@ -1209,18 +1262,18 @@ export const reviewsApi = {
|
||||
// 替换评委
|
||||
replaceJudge: async (
|
||||
assignmentId: number,
|
||||
newJudgeId: number
|
||||
newJudgeId: number,
|
||||
): Promise<void> => {
|
||||
await request.post<any, void>(
|
||||
`/contests/reviews/replace-judge`,
|
||||
{ assignmentId, newJudgeId }
|
||||
);
|
||||
await request.post<any, void>(`/contests/reviews/replace-judge`, {
|
||||
assignmentId,
|
||||
newJudgeId,
|
||||
});
|
||||
},
|
||||
|
||||
// 获取评委参与的活动列表
|
||||
getJudgeContests: async (): Promise<any[]> => {
|
||||
const response = await request.get<any, any[]>(
|
||||
`/contests/reviews/judge/contests`
|
||||
`/contests/reviews/judge/contests`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1234,8 +1287,13 @@ export const reviewsApi = {
|
||||
workNo?: string;
|
||||
accountNo?: string;
|
||||
reviewStatus?: string;
|
||||
}
|
||||
): Promise<{ list: any[]; total: number; page: number; pageSize: number }> => {
|
||||
},
|
||||
): Promise<{
|
||||
list: any[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}> => {
|
||||
const response = await request.get<
|
||||
any,
|
||||
{ list: any[]; total: number; page: number; pageSize: number }
|
||||
@ -1246,7 +1304,7 @@ export const reviewsApi = {
|
||||
// 获取评委视角的赛事详情(含评审规则)
|
||||
getJudgeContestDetail: async (contestId: number): Promise<any> => {
|
||||
const response = await request.get<any, any>(
|
||||
`/contests/reviews/judge/contests/${contestId}/detail`
|
||||
`/contests/reviews/judge/contests/${contestId}/detail`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1257,7 +1315,7 @@ export const noticesApi = {
|
||||
// 获取公告列表(按活动)
|
||||
getList: async (contestId: number): Promise<ContestNotice[]> => {
|
||||
const response = await request.get<any, ContestNotice[]>(
|
||||
`/contests/notices/contest/${contestId}`
|
||||
`/contests/notices/contest/${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1270,8 +1328,8 @@ export const noticesApi = {
|
||||
pageSize?: number;
|
||||
}): Promise<PaginationResponse<ContestNotice>> => {
|
||||
const response = await request.get<any, PaginationResponse<ContestNotice>>(
|
||||
'/contests/notices',
|
||||
{ params }
|
||||
"/contests/notices",
|
||||
{ params },
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1279,7 +1337,7 @@ export const noticesApi = {
|
||||
// 获取公告详情
|
||||
getDetail: async (id: number): Promise<ContestNotice> => {
|
||||
const response = await request.get<any, ContestNotice>(
|
||||
`/contests/notices/${id}`
|
||||
`/contests/notices/${id}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1288,7 +1346,7 @@ export const noticesApi = {
|
||||
create: async (data: CreateNoticeForm): Promise<ContestNotice> => {
|
||||
const response = await request.post<any, ContestNotice>(
|
||||
"/contests/notices",
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1296,11 +1354,11 @@ export const noticesApi = {
|
||||
// 更新公告
|
||||
update: async (
|
||||
id: number,
|
||||
data: Partial<CreateNoticeForm>
|
||||
data: Partial<CreateNoticeForm>,
|
||||
): Promise<ContestNotice> => {
|
||||
const response = await request.patch<any, ContestNotice>(
|
||||
`/contests/notices/${id}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1373,7 +1431,7 @@ export interface SetAwardForm {
|
||||
export interface BatchSetAwardsForm {
|
||||
awards: Array<{
|
||||
workId: number;
|
||||
awardLevel: 'first' | 'second' | 'third' | 'excellent' | 'none';
|
||||
awardLevel: "first" | "second" | "third" | "excellent" | "none";
|
||||
awardName?: string;
|
||||
}>;
|
||||
}
|
||||
@ -1385,44 +1443,61 @@ export interface AutoSetAwardsForm {
|
||||
// 成果管理
|
||||
export const resultsApi = {
|
||||
// 计算所有作品的最终得分
|
||||
calculateScores: async (contestId: number): Promise<{ message: string; calculatedCount: number; calculationRule: string }> => {
|
||||
calculateScores: async (
|
||||
contestId: number,
|
||||
): Promise<{
|
||||
message: string;
|
||||
calculatedCount: number;
|
||||
calculationRule: string;
|
||||
}> => {
|
||||
const response = await request.post<any, any>(
|
||||
`/contests/results/${contestId}/calculate-scores`
|
||||
`/contests/results/${contestId}/calculate-scores`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 计算排名
|
||||
calculateRankings: async (contestId: number): Promise<{ message: string; rankedCount: number }> => {
|
||||
calculateRankings: async (
|
||||
contestId: number,
|
||||
): Promise<{ message: string; rankedCount: number }> => {
|
||||
const response = await request.post<any, any>(
|
||||
`/contests/results/${contestId}/calculate-rankings`
|
||||
`/contests/results/${contestId}/calculate-rankings`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 设置单个作品奖项
|
||||
setAward: async (workId: number, data: SetAwardForm): Promise<ContestResult> => {
|
||||
setAward: async (
|
||||
workId: number,
|
||||
data: SetAwardForm,
|
||||
): Promise<ContestResult> => {
|
||||
const response = await request.patch<any, ContestResult>(
|
||||
`/contests/results/work/${workId}/award`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 批量设置奖项
|
||||
batchSetAwards: async (contestId: number, data: BatchSetAwardsForm): Promise<any> => {
|
||||
batchSetAwards: async (
|
||||
contestId: number,
|
||||
data: BatchSetAwardsForm,
|
||||
): Promise<any> => {
|
||||
const response = await request.post<any, any>(
|
||||
`/contests/results/${contestId}/batch-set-awards`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 根据排名自动设置奖项
|
||||
autoSetAwards: async (contestId: number, data: AutoSetAwardsForm): Promise<any> => {
|
||||
autoSetAwards: async (
|
||||
contestId: number,
|
||||
data: AutoSetAwardsForm,
|
||||
): Promise<any> => {
|
||||
const response = await request.post<any, any>(
|
||||
`/contests/results/${contestId}/auto-set-awards`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1430,7 +1505,7 @@ export const resultsApi = {
|
||||
// 发布成果
|
||||
publish: async (contestId: number): Promise<any> => {
|
||||
const response = await request.post<any, any>(
|
||||
`/contests/results/${contestId}/publish`
|
||||
`/contests/results/${contestId}/publish`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1438,7 +1513,7 @@ export const resultsApi = {
|
||||
// 撤回发布
|
||||
unpublish: async (contestId: number): Promise<any> => {
|
||||
const response = await request.post<any, any>(
|
||||
`/contests/results/${contestId}/unpublish`
|
||||
`/contests/results/${contestId}/unpublish`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1451,11 +1526,17 @@ export const resultsApi = {
|
||||
pageSize?: number;
|
||||
workNo?: string;
|
||||
accountNo?: string;
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<ResultsResponse> => {
|
||||
const response = await request.get<any, ResultsResponse>(
|
||||
`/contests/results/${contestId}`,
|
||||
{ params: { page: params.page || 1, pageSize: params.pageSize || 10, ...params } }
|
||||
{
|
||||
params: {
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 10,
|
||||
...params,
|
||||
},
|
||||
},
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1463,7 +1544,7 @@ export const resultsApi = {
|
||||
// 获取活动结果统计摘要
|
||||
getSummary: async (contestId: number): Promise<ResultsSummary> => {
|
||||
const response = await request.get<any, ResultsSummary>(
|
||||
`/contests/results/${contestId}/summary`
|
||||
`/contests/results/${contestId}/summary`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1471,10 +1552,12 @@ export const resultsApi = {
|
||||
|
||||
// 评委管理
|
||||
export const judgesApi = {
|
||||
// 获取评委列表
|
||||
getList: async (contestId: number): Promise<ContestJudge[]> => {
|
||||
const response = await request.get<any, ContestJudge[]>(
|
||||
`/contests/judges/contest/${contestId}`
|
||||
// 获取赛事评委(assigned + implicitPool)
|
||||
getList: async (
|
||||
contestId: number,
|
||||
): Promise<ContestJudgesForContestResponse> => {
|
||||
const response = await request.get<any, ContestJudgesForContestResponse>(
|
||||
`/contests/judges/contest/${contestId}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1482,7 +1565,7 @@ export const judgesApi = {
|
||||
// 获取评委详情
|
||||
getDetail: async (id: number): Promise<ContestJudge> => {
|
||||
const response = await request.get<any, ContestJudge>(
|
||||
`/contests/judges/${id}`
|
||||
`/contests/judges/${id}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1491,7 +1574,7 @@ export const judgesApi = {
|
||||
create: async (data: CreateJudgeForm): Promise<ContestJudge> => {
|
||||
const response = await request.post<any, ContestJudge>(
|
||||
"/contests/judges",
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
@ -1499,11 +1582,11 @@ export const judgesApi = {
|
||||
// 更新评委
|
||||
update: async (
|
||||
id: number,
|
||||
data: Partial<CreateJudgeForm>
|
||||
data: Partial<CreateJudgeForm>,
|
||||
): Promise<ContestJudge> => {
|
||||
const response = await request.patch<any, ContestJudge>(
|
||||
`/contests/judges/${id}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
|
||||
@ -29,7 +29,8 @@
|
||||
<span class="progress-text">{{ record.reviewed }}/{{ record.totalAssigned }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewStatus'">
|
||||
<a-tag v-if="record.pending === 0" color="success">已完成</a-tag>
|
||||
<a-tag v-if="record.totalAssigned > 0 && record.pending === 0" color="success">已完成</a-tag>
|
||||
<a-tag v-else-if="record.totalAssigned === 0" color="default">无分配</a-tag>
|
||||
<a-tag v-else color="processing">评审中</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
@ -99,8 +100,9 @@ const getProgressPercent = (record: any) => {
|
||||
return Math.round((record.reviewed / record.totalAssigned) * 100)
|
||||
}
|
||||
|
||||
// 获取进度状态
|
||||
// 获取进度状态(无分配时不显示 success,避免与「无分配」矛盾)
|
||||
const getProgressStatus = (record: any) => {
|
||||
if (record.totalAssigned === 0) return "normal"
|
||||
if (record.pending === 0) return "success"
|
||||
return "active"
|
||||
}
|
||||
|
||||
@ -72,7 +72,9 @@
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'workNo'">
|
||||
<a @click="handleViewWork(record)">{{ record.workNo || "-" }}</a>
|
||||
<a @click="handleViewWork(record)">{{
|
||||
record.workNo || (record.workId != null ? `#${record.workId}` : "-")
|
||||
}}</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'score'">
|
||||
<span v-if="record.totalScore != null" class="score">
|
||||
|
||||
@ -4,30 +4,24 @@
|
||||
<a-card class="mb-4" size="small">
|
||||
<a-form layout="inline" :model="searchParams" @finish="handleSearch">
|
||||
<a-form-item label="姓名">
|
||||
<a-input
|
||||
v-model:value="searchParams.nickname"
|
||||
placeholder="请输入姓名"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<a-input v-model:value="searchParams.nickname" placeholder="请输入姓名" allow-clear style="width: 150px"
|
||||
@press-enter="handleSearch" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所属单位">
|
||||
<a-input
|
||||
v-model:value="searchParams.organization"
|
||||
placeholder="请输入所属单位"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<a-input v-model:value="searchParams.organization" placeholder="请输入所属单位" allow-clear style="width: 200px"
|
||||
@press-enter="handleSearch" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
@ -37,22 +31,14 @@
|
||||
<!-- 全部评委列表 -->
|
||||
<a-card class="mb-4" size="small">
|
||||
<template #title>全部评委</template>
|
||||
<a-table
|
||||
:columns="judgeColumns"
|
||||
:data-source="judgeList"
|
||||
:loading="judgeLoading"
|
||||
:pagination="judgePagination"
|
||||
<a-table :columns="judgeColumns" :data-source="judgeList" :loading="judgeLoading" :pagination="judgePagination"
|
||||
:row-selection="{
|
||||
selectedRowKeys: selectedJudgeIds,
|
||||
onChange: handleJudgeSelectionChange,
|
||||
getCheckboxProps: (record: Judge) => ({
|
||||
disabled: isJudgeSelected(record.id),
|
||||
}),
|
||||
}"
|
||||
row-key="id"
|
||||
size="small"
|
||||
@change="handleJudgeTableChange"
|
||||
>
|
||||
}" row-key="id" size="small" @change="handleJudgeTableChange">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'organization'">
|
||||
{{ record.organization || "-" }}
|
||||
@ -69,19 +55,18 @@
|
||||
<!-- 已选评委区域 -->
|
||||
<a-card size="small">
|
||||
<template #title>
|
||||
已选 {{ selectedJudges.length }} / {{ judgeCount || 0 }}
|
||||
<span
|
||||
v-if="selectedJudges.length > (judgeCount || 0)"
|
||||
class="warning-text"
|
||||
>
|
||||
(超出{{ selectedJudges.length - (judgeCount || 0) }}人)
|
||||
</span>
|
||||
<template v-if="judgeCount > 0">
|
||||
已选 {{ selectedJudges.length }} / {{ judgeCount }}
|
||||
<span v-if="selectedJudges.length > judgeCount" class="warning-text">
|
||||
(超出{{ selectedJudges.length - judgeCount }}人)
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
已选 {{ selectedJudges.length }}
|
||||
<span class="hint-text">(评审规则未配置人数)</span>
|
||||
</template>
|
||||
</template>
|
||||
<a-list
|
||||
:data-source="selectedJudges"
|
||||
:loading="selectedJudgesLoading"
|
||||
size="small"
|
||||
>
|
||||
<a-list :data-source="selectedJudges" :loading="selectedJudgesLoading" size="small">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
@ -93,12 +78,7 @@
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleRemoveJudge(item.id)"
|
||||
>
|
||||
<a-button type="link" danger size="small" @click="handleRemoveJudge(item.id)">
|
||||
移除
|
||||
</a-button>
|
||||
</template>
|
||||
@ -114,12 +94,7 @@
|
||||
<div class="drawer-footer">
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="selectedJudges.length === 0"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<a-button type="primary" :loading="submitLoading" :disabled="submitLoading" @click="handleSubmit">
|
||||
确定
|
||||
</a-button>
|
||||
</a-space>
|
||||
@ -235,9 +210,8 @@ const loadJudges = async () => {
|
||||
const loadSelectedJudges = async () => {
|
||||
selectedJudgesLoading.value = true
|
||||
try {
|
||||
const judges = await judgesApi.getList(props.contestId)
|
||||
// 提取评委ID和评委信息
|
||||
const judgeIds = judges.map((j) => j.judgeId)
|
||||
const { assigned } = await judgesApi.getList(props.contestId)
|
||||
const judgeIds = assigned.map((j) => j.judgeId)
|
||||
selectedJudgeIds.value = judgeIds
|
||||
|
||||
// 获取评委详细信息
|
||||
@ -317,40 +291,48 @@ const handleJudgeTableChange = (pag: any) => {
|
||||
loadJudges()
|
||||
}
|
||||
|
||||
// 提交
|
||||
// 提交(与后端 assigned 对比:删除取消勾选的关联行 + 新增勾选项)
|
||||
const handleSubmit = async () => {
|
||||
if (selectedJudges.value.length === 0) {
|
||||
message.warning("请至少选择一个评委")
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
// 获取当前已选评委ID
|
||||
const currentJudgeIds = await judgesApi
|
||||
.getList(props.contestId)
|
||||
.then((judges) => judges.map((j) => j.judgeId))
|
||||
const { assigned } = await judgesApi.getList(props.contestId)
|
||||
const currentJudgeIds = assigned.map((j) => j.judgeId)
|
||||
|
||||
const toRemoveContestJudgeIds = assigned
|
||||
.filter(
|
||||
(j) => j.id != null && !selectedJudgeIds.value.includes(j.judgeId),
|
||||
)
|
||||
.map((j) => j.id as number)
|
||||
|
||||
// 找出需要添加的评委(新选中的,但之前未添加的)
|
||||
const toAddIds = selectedJudgeIds.value.filter(
|
||||
(id) => !currentJudgeIds.includes(id)
|
||||
(id) => !currentJudgeIds.includes(id),
|
||||
)
|
||||
|
||||
// 批量添加评委
|
||||
if (toRemoveContestJudgeIds.length === 0 && toAddIds.length === 0) {
|
||||
message.info("没有变更")
|
||||
return
|
||||
}
|
||||
|
||||
if (toRemoveContestJudgeIds.length > 0) {
|
||||
await Promise.all(
|
||||
toRemoveContestJudgeIds.map((id) => judgesApi.delete(id)),
|
||||
)
|
||||
}
|
||||
|
||||
if (toAddIds.length > 0) {
|
||||
await Promise.all(
|
||||
toAddIds.map((judgeId) =>
|
||||
judgesApi.create({
|
||||
contestId: props.contestId,
|
||||
judgeId: judgeId,
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
emit("success")
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "添加评委失败")
|
||||
message.error(error?.response?.data?.message || "保存评委失败")
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
@ -396,6 +378,12 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
||||
@ -85,16 +85,15 @@
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'workNo'">
|
||||
<a @click="handleViewWorkDetail(record)">{{ record.workNo || "-" }}</a>
|
||||
<a @click="handleViewWorkDetail(record)">{{
|
||||
record.workNo || (record.id != null ? `#${record.id}` : "-")
|
||||
}}</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'username'">
|
||||
{{ record.submitterAccountNo || record.registration?.user?.username || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'judgeScore'">
|
||||
<span v-if="record.averageScore !== undefined && record.averageScore !== null">
|
||||
{{ record.averageScore.toFixed(2) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
{{ formatWorkJudgeScore(record) ?? "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewProgress'">
|
||||
<a-tag :color="getProgressColor(record)">
|
||||
@ -121,12 +120,17 @@
|
||||
:data-source="scoreList"
|
||||
:loading="scoreLoading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
row-key="scoreId"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'judgeName'">
|
||||
{{ record.judge?.nickname || record.judge?.username || "-" }}
|
||||
{{
|
||||
record.judgeName ||
|
||||
record.judge?.nickname ||
|
||||
record.judge?.username ||
|
||||
"-"
|
||||
}}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'phone'">
|
||||
{{ record.judge?.phone || "-" }}
|
||||
@ -402,6 +406,13 @@ const formatDate = (dateStr?: string) => {
|
||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||
}
|
||||
|
||||
/** 主表「评委评分」:列表接口优先返回 finalScore,兼容 averageScore */
|
||||
const formatWorkJudgeScore = (record: ContestWork): string | null => {
|
||||
const v = record.finalScore ?? record.averageScore
|
||||
if (v === undefined || v === null) return null
|
||||
return Number(v).toFixed(2)
|
||||
}
|
||||
|
||||
// 获取活动信息
|
||||
const fetchContestInfo = async () => {
|
||||
try {
|
||||
@ -607,7 +618,10 @@ const handleConfirmReplace = async () => {
|
||||
replaceLoading.value = true
|
||||
try {
|
||||
// TODO: 调用替换评委的API
|
||||
await reviewsApi.replaceJudge(currentReplaceScore.value.id, selectedJudgeRow.value.id)
|
||||
await reviewsApi.replaceJudge(
|
||||
currentReplaceScore.value.assignmentId ?? currentReplaceScore.value.id,
|
||||
selectedJudgeRow.value.id,
|
||||
)
|
||||
message.success("替换成功")
|
||||
replaceJudgeDrawerVisible.value = false
|
||||
// 刷新评分列表
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user