feat: 公众端活动成果卡片展示与公开公示接口
Made-with: Cursor
This commit is contained in:
parent
328533e805
commit
593f7977eb
@ -25,5 +25,10 @@ public interface IContestResultService {
|
|||||||
|
|
||||||
PageResult<Map<String, Object>> getResults(Long contestId, Long page, Long pageSize, String workNo, String accountNo);
|
PageResult<Map<String, Object>> getResults(Long contestId, Long page, Long pageSize, String workNo, String accountNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公众端公示列表:仅活动公开且成果已发布;不含报名账号等敏感字段。
|
||||||
|
*/
|
||||||
|
PageResult<Map<String, Object>> getPublicResults(Long contestId, Long page, Long pageSize);
|
||||||
|
|
||||||
Map<String, Object> getResultsSummary(Long contestId);
|
Map<String, Object> getResultsSummary(Long contestId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.competition.common.enums.ErrorCode;
|
import com.competition.common.enums.ErrorCode;
|
||||||
import com.competition.common.enums.PublishStatus;
|
import com.competition.common.enums.PublishStatus;
|
||||||
|
import com.competition.common.enums.Visibility;
|
||||||
import com.competition.common.exception.BusinessException;
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.common.result.PageResult;
|
import com.competition.common.result.PageResult;
|
||||||
import com.competition.modules.biz.contest.entity.BizContest;
|
import com.competition.modules.biz.contest.entity.BizContest;
|
||||||
@ -368,6 +369,81 @@ public class ContestResultServiceImpl implements IContestResultService {
|
|||||||
return PageResult.from(result, voList);
|
return PageResult.from(result, voList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<Map<String, Object>> getPublicResults(Long contestId, Long page, Long pageSize) {
|
||||||
|
BizContest contest = contestMapper.selectById(contestId);
|
||||||
|
if (contest == null) {
|
||||||
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
|
||||||
|
}
|
||||||
|
if (!Visibility.PUBLIC.getValue().equals(contest.getVisibility())) {
|
||||||
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
|
||||||
|
}
|
||||||
|
if (!PublishStatus.PUBLISHED.getValue().equals(contest.getResultState())) {
|
||||||
|
throw BusinessException.of(ErrorCode.BAD_REQUEST, "成果未发布");
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(BizContestWork::getContestId, contestId);
|
||||||
|
wrapper.eq(BizContestWork::getIsLatest, true);
|
||||||
|
wrapper.eq(BizContestWork::getValidState, 1);
|
||||||
|
wrapper.isNotNull(BizContestWork::getFinalScore);
|
||||||
|
wrapper.orderByDesc(BizContestWork::getFinalScore);
|
||||||
|
|
||||||
|
Page<BizContestWork> pageObj = new Page<>(page, pageSize);
|
||||||
|
Page<BizContestWork> result = workMapper.selectPage(pageObj, wrapper);
|
||||||
|
|
||||||
|
List<BizContestWork> records = result.getRecords();
|
||||||
|
Set<Long> registrationIds = records.stream()
|
||||||
|
.map(BizContestWork::getRegistrationId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Map<Long, BizContestRegistration> registrationById = new HashMap<>();
|
||||||
|
if (!registrationIds.isEmpty()) {
|
||||||
|
for (BizContestRegistration reg : contestRegistrationMapper.selectBatchIds(registrationIds)) {
|
||||||
|
registrationById.put(reg.getId(), reg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> voList = records.stream().map(w -> {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("id", w.getId());
|
||||||
|
map.put("workNo", w.getWorkNo());
|
||||||
|
map.put("title", w.getTitle());
|
||||||
|
map.put("finalScore", w.getFinalScore());
|
||||||
|
map.put("rank", w.getRank());
|
||||||
|
map.put("awardName", w.getAwardName());
|
||||||
|
BizContestRegistration reg = w.getRegistrationId() != null
|
||||||
|
? registrationById.get(w.getRegistrationId())
|
||||||
|
: null;
|
||||||
|
map.put("participantName", buildPublicParticipantName(contest, reg));
|
||||||
|
return map;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return PageResult.from(result, voList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公众展示用作者名:团队赛优先队名,否则为报名展示名(非登录账号)。
|
||||||
|
*/
|
||||||
|
private String buildPublicParticipantName(BizContest contest, BizContestRegistration reg) {
|
||||||
|
if (reg == null) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
if ("team".equals(contest.getContestType())) {
|
||||||
|
if (StringUtils.hasText(reg.getTeamName())) {
|
||||||
|
return reg.getTeamName();
|
||||||
|
}
|
||||||
|
return StringUtils.hasText(reg.getAccountName()) ? reg.getAccountName() : "-";
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(reg.getAccountName())) {
|
||||||
|
return reg.getAccountName();
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(reg.getTeamName())) {
|
||||||
|
return reg.getTeamName();
|
||||||
|
}
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getResultsSummary(Long contestId) {
|
public Map<String, Object> getResultsSummary(Long contestId) {
|
||||||
log.info("查询赛果概览,赛事ID:{}", contestId);
|
log.info("查询赛果概览,赛事ID:{}", contestId);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import com.competition.common.util.SecurityUtil;
|
|||||||
import com.competition.modules.biz.contest.entity.BizContest;
|
import com.competition.modules.biz.contest.entity.BizContest;
|
||||||
import com.competition.modules.biz.contest.entity.BizContestRegistration;
|
import com.competition.modules.biz.contest.entity.BizContestRegistration;
|
||||||
import com.competition.modules.biz.contest.entity.BizContestWork;
|
import com.competition.modules.biz.contest.entity.BizContestWork;
|
||||||
|
import com.competition.modules.biz.review.service.IContestResultService;
|
||||||
import com.competition.modules.pub.dto.PublicRegisterActivityDto;
|
import com.competition.modules.pub.dto.PublicRegisterActivityDto;
|
||||||
import com.competition.modules.pub.service.PublicActivityService;
|
import com.competition.modules.pub.service.PublicActivityService;
|
||||||
import com.competition.security.annotation.Public;
|
import com.competition.security.annotation.Public;
|
||||||
@ -23,6 +24,7 @@ import java.util.Map;
|
|||||||
public class PublicActivityController {
|
public class PublicActivityController {
|
||||||
|
|
||||||
private final PublicActivityService publicActivityService;
|
private final PublicActivityService publicActivityService;
|
||||||
|
private final IContestResultService contestResultService;
|
||||||
|
|
||||||
@Public
|
@Public
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@ -42,6 +44,16 @@ public class PublicActivityController {
|
|||||||
return Result.success(publicActivityService.getActivityDetail(id));
|
return Result.success(publicActivityService.getActivityDetail(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public
|
||||||
|
@GetMapping("/{id}/results")
|
||||||
|
@Operation(summary = "公示成果列表(仅公开活动且成果已发布,无需登录)")
|
||||||
|
public Result<PageResult<Map<String, Object>>> getPublishedResults(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam(defaultValue = "1") Long page,
|
||||||
|
@RequestParam(defaultValue = "10") Long pageSize) {
|
||||||
|
return Result.success(contestResultService.getPublicResults(id, page, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/my-registration")
|
@GetMapping("/{id}/my-registration")
|
||||||
@Operation(summary = "查询我的报名信息")
|
@Operation(summary = "查询我的报名信息")
|
||||||
public Result<Map<String, Object>> getMyRegistration(@PathVariable Long id) {
|
public Result<Map<String, Object>> getMyRegistration(@PathVariable Long id) {
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
| [UGC社区升级](./public/ugc-platform-upgrade.md) | 整体架构 | 需求已确认 | 2026-03-27 |
|
| [UGC社区升级](./public/ugc-platform-upgrade.md) | 整体架构 | 需求已确认 | 2026-03-27 |
|
||||||
| [UGC开发计划](./public/ugc-development-plan.md) | 开发排期 | P0已完成,P1进行中 | 2026-03-31 |
|
| [UGC开发计划](./public/ugc-development-plan.md) | 开发排期 | P0已完成,P1进行中 | 2026-03-31 |
|
||||||
| [点赞&收藏](./public/like-favorite.md) | 社区互动 | 已实现 | 2026-03-31 |
|
| [点赞&收藏](./public/like-favorite.md) | 社区互动 | 已实现 | 2026-03-31 |
|
||||||
|
| [公众端活动成果展示](./public/activity-results-public-display.md) | 活动详情 / 成果 Tab | 需求已补充,待开发 | 2026-04-08 |
|
||||||
|
|
||||||
## 评委端
|
## 评委端
|
||||||
|
|
||||||
|
|||||||
156
docs/design/public/activity-results-public-display.md
Normal file
156
docs/design/public/activity-results-public-display.md
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# 公众端活动详情 —「活动成果」Tab 设计方案
|
||||||
|
|
||||||
|
> 所属端:用户端(公众端 `/p/`)
|
||||||
|
> 状态:需求已补充,待开发实现
|
||||||
|
> 创建日期:2026-04-08
|
||||||
|
> 关联需求:[总需求 US-105](../../project/01-requirements.md)、[开发计划 P1 成果查看详情](../../project/05-development-plan.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
### 1.1 问题
|
||||||
|
|
||||||
|
机构在管理端「成果发布」中发布活动成果后,参与者与公众需要在**公众端活动详情页**查看评审结果。当前实现仅在「活动成果」Tab 内展示奖杯图标与提示语「活动成果已发布,敬请查看!」,**未展示具体获奖名单与得分**,与总需求中「查看活动成果和评审结果」不一致。
|
||||||
|
|
||||||
|
### 1.2 目标
|
||||||
|
|
||||||
|
- 在**不降低隐私安全**的前提下,使公众端 Tab 内容与管理端已发布成果的**业务口径一致**(排名、奖项、分数来源同一套计算与数据)。
|
||||||
|
- 将「活动成果已发布,敬请查看!」定位为**辅助提示**(如副标题或加载态提示),**主内容**为可浏览的成果列表及必要元信息。
|
||||||
|
|
||||||
|
### 1.3 关联文档
|
||||||
|
|
||||||
|
| 文档 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| [成果发布优化(超管)](../super-admin/results-publish-optimization.md) | 活动维度发布状态、列表筛选 |
|
||||||
|
| [租户端全面优化 — 成果发布 Detail](../org-admin/tenant-portal-optimization.md) | 统计摘要、排名、设奖、发布流程 |
|
||||||
|
| [UGC 社区升级](./ugc-platform-upgrade.md) | 公众端整体信息架构,本设计细化「活动」模块下成果 Tab |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 入口与可见性
|
||||||
|
|
||||||
|
### 2.1 Tab 显示条件
|
||||||
|
|
||||||
|
- 仅当活动 **`resultState === 'published'`** 时显示「活动成果」Tab。
|
||||||
|
- 未发布:不展示该 Tab(与当前 [ActivityDetail.vue](../../../frontend/src/views/public/ActivityDetail.vue) 逻辑一致)。
|
||||||
|
|
||||||
|
### 2.2 主操作区「查看成果」
|
||||||
|
|
||||||
|
- 活动阶段为「已结束」时,主按钮「查看成果」将 **`activeTab` 切换为 `results`**,用户在同一页查看「活动成果」Tab 内容。
|
||||||
|
- 文档约定:**默认不跳转独立路由**;若未来产品改为「成果专页」,需单独迭代路由与返回行为。
|
||||||
|
|
||||||
|
### 2.3 是否需要登录
|
||||||
|
|
||||||
|
| 策略 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **默认(推荐)** | **无需登录**即可查看已发布活动的成果列表,与公开浏览活动信息一致。 |
|
||||||
|
| 可选 | 若合规或机构要求「仅报名用户可见」,可在活动级增加配置,此时未登录用户看到引导登录或模糊提示;**需在实现阶段单独评审**,本文档以默认策略为验收基线。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Tab 内信息架构
|
||||||
|
|
||||||
|
### 3.1 文案「活动成果已发布,敬请查看!」的定位
|
||||||
|
|
||||||
|
- **不作为唯一内容**:数据加载完成后,主区域应为成果列表。
|
||||||
|
- 可用作:**副文案**、**骨架屏/加载中的简短说明**,或列表上方的温和提示。
|
||||||
|
|
||||||
|
### 3.2 主内容:成果列表
|
||||||
|
|
||||||
|
与管理端活动详情「活动结果」Tab([contests/Detail.vue](../../../frontend/src/views/contests/Detail.vue))**同一业务数据口径**,公众端字段可裁剪如下:
|
||||||
|
|
||||||
|
| 字段 | 是否展示(默认) | 说明 |
|
||||||
|
|------|------------------|------|
|
||||||
|
| 名次(排名) | 是 | 与管理端排名一致 |
|
||||||
|
| 作品编号 | 是 | 若有业务编号则展示,便于用户核对 |
|
||||||
|
| 奖项 | 是 | 如一等奖、二等奖等 |
|
||||||
|
| 最终得分 | 是 | 与机构发布策略一致;若机构侧不公开分数,后端可不返回或由配置控制 |
|
||||||
|
| 姓名/展示名 | 是 | 个人:用户昵称;团队:队名 |
|
||||||
|
| 报名账号 | **否** | 见第 4 节 |
|
||||||
|
|
||||||
|
### 3.3 元信息
|
||||||
|
|
||||||
|
- **成果发布时间**:展示活动级 **`resultPublishTime`**(公众活动详情接口已返回该字段,见前端 `PublicActivity` 类型)。
|
||||||
|
|
||||||
|
### 3.4 空状态
|
||||||
|
|
||||||
|
- 当 `resultState === 'published'` 但公示列表为空时:展示明确空状态文案,例如「暂无公示信息」,避免长时间加载无反馈。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 隐私与脱敏
|
||||||
|
|
||||||
|
- **默认不展示**:登录账号、手机号、邮箱等可直接识别身份或联系方式的字段。
|
||||||
|
- **展示名**:优先使用昵称、队名等已在业务中用于对外的名称。
|
||||||
|
- **少儿场景**:不展示精确联系方式;仅展示机构允许公示的字段。
|
||||||
|
- 若需展示账号类信息,须为**脱敏**形式(如仅保留前后若干位),且需产品/合规确认。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 交互与体验
|
||||||
|
|
||||||
|
### 5.1 列表与分页
|
||||||
|
|
||||||
|
- 移动端优先:采用 **分页** 或 **加载更多** 之一,避免单次加载数据量过大。
|
||||||
|
- 列表项在窄屏下可用卡片式排布,保证名次、奖项、得分可读。
|
||||||
|
|
||||||
|
### 5.2 行内「查看详情」(分期)
|
||||||
|
|
||||||
|
| 阶段 | 范围 |
|
||||||
|
|------|------|
|
||||||
|
| **P1** | 完成公示**列表**(排名、奖项、得分、展示名、作品编号等)。 |
|
||||||
|
| **P2(可选)** | 点击行进入参赛作品/绘本预览详情,依赖作品公开范围与权限策略。 |
|
||||||
|
|
||||||
|
### 5.3 数据一致性
|
||||||
|
|
||||||
|
- 排名、奖项、最终得分的**计算与更新**与租户端「成果发布」模块一致,避免公众端与后台两套数据。参见 [租户端成果发布 Detail](../org-admin/tenant-portal-optimization.md)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 后端与接口(需求说明)
|
||||||
|
|
||||||
|
### 6.1 现状
|
||||||
|
|
||||||
|
- 管理端使用 `GET /contests/results/{contestId}`(需 **`contest:read`**),**不适合**匿名公众直接调用。
|
||||||
|
|
||||||
|
### 6.2 建议新增(实现时)
|
||||||
|
|
||||||
|
- 提供公众专用只读接口,例如:
|
||||||
|
**`GET /public/activities/{id}/results`**
|
||||||
|
- 查询参数:分页(`page` / `pageSize`),可选关键词(若需按作品编号搜索可后续扩展)。
|
||||||
|
- **仅当**该活动 `resultState === 'published'` 时返回公示字段;否则返回空列表或 **404**,具体策略在实现时二选一并写清(推荐与详情接口状态一致:未发布则列表接口返回空列表 + 前端不展示 Tab,减少歧义)。
|
||||||
|
|
||||||
|
### 6.3 多租户与安全
|
||||||
|
|
||||||
|
- 服务端必须按活动 **`tenant_id`** 与 **`contest_id`** 限定查询,仅返回该活动下已发布结果数据,防止跨租户泄露。
|
||||||
|
- 不依赖前端传 `tenantId` 作为信任来源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 验收标准
|
||||||
|
|
||||||
|
| 编号 | 场景 | 预期 |
|
||||||
|
|------|------|------|
|
||||||
|
| AC-1 | 成果已发布 | 公众端「活动成果」Tab 展示与文档一致的列表字段,不含未授权敏感信息 |
|
||||||
|
| AC-2 | 成果未发布 | 不展示「活动成果」Tab |
|
||||||
|
| AC-3 | 机构撤回发布 | 活动 `resultState` 非 `published` 后,公众端不再展示该 Tab 或列表内容 |
|
||||||
|
| AC-4 | 匿名用户 | 默认策略下,无需登录即可查看已发布列表 |
|
||||||
|
| AC-5 | 发布时间 | 可看到与活动一致的成果发布时间(若接口与 UI 已接通) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 前端实现参考(待开发)
|
||||||
|
|
||||||
|
| 位置 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| [frontend/src/views/public/ActivityDetail.vue](../../../frontend/src/views/public/ActivityDetail.vue) | 替换纯占位为列表 + 加载/空状态;接入公众成果 API |
|
||||||
|
| [frontend/src/api/public.ts](../../../frontend/src/api/public.ts) | 新增 `publicActivitiesApi.getPublishedResults(contestId, params)` 等 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 修订记录
|
||||||
|
|
||||||
|
| 日期 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 2026-04-08 | 初稿:入口、字段、隐私、接口、验收与分期 |
|
||||||
@ -262,6 +262,17 @@ export interface PublicActivityDetail extends PublicActivity {
|
|||||||
targetCities?: string[]
|
targetCities?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 公众端公示成果行(无报名账号等敏感字段) */
|
||||||
|
export interface PublicActivityResultItem {
|
||||||
|
id: number
|
||||||
|
workNo: string | null
|
||||||
|
title: string | null
|
||||||
|
rank: number | null
|
||||||
|
finalScore: number | string | null
|
||||||
|
awardName: string | null
|
||||||
|
participantName: string
|
||||||
|
}
|
||||||
|
|
||||||
export const publicActivitiesApi = {
|
export const publicActivitiesApi = {
|
||||||
list: (params?: {
|
list: (params?: {
|
||||||
page?: number
|
page?: number
|
||||||
@ -303,6 +314,17 @@ export const publicActivitiesApi = {
|
|||||||
attachments?: { fileName: string; fileUrl: string; fileType?: string; size?: string }[]
|
attachments?: { fileName: string; fileUrl: string; fileType?: string; size?: string }[]
|
||||||
},
|
},
|
||||||
) => publicApi.post(`/public/activities/${id}/submit-work`, data),
|
) => publicApi.post(`/public/activities/${id}/submit-work`, data),
|
||||||
|
|
||||||
|
/** 公示成果分页(仅 resultState=published 的活动;无需登录) */
|
||||||
|
getPublishedResults: (
|
||||||
|
id: number,
|
||||||
|
params?: { page?: number; pageSize?: number },
|
||||||
|
): Promise<{
|
||||||
|
list: PublicActivityResultItem[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}> => publicApi.get(`/public/activities/${id}/results`, { params }),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 我的报名 ====================
|
// ==================== 我的报名 ====================
|
||||||
|
|||||||
@ -131,9 +131,71 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="results" tab="活动成果" v-if="activity.resultState === 'published'">
|
<a-tab-pane key="results" tab="活动成果" v-if="activity.resultState === 'published'">
|
||||||
<div class="results-hint">
|
<div class="results-panel">
|
||||||
<trophy-outlined class="results-icon" />
|
<div class="results-hero">
|
||||||
<p>活动成果已发布,敬请查看!</p>
|
<trophy-outlined class="results-icon" />
|
||||||
|
<p class="results-hero-text">活动成果已发布</p>
|
||||||
|
</div>
|
||||||
|
<p v-if="activity.resultPublishTime" class="results-publish-time">
|
||||||
|
发布时间:{{ formatDateTime(activity.resultPublishTime) }}
|
||||||
|
</p>
|
||||||
|
<p class="results-hint-line">以下排名与得分以主办方发布为准。</p>
|
||||||
|
<a-spin :spinning="resultsLoading">
|
||||||
|
<template v-if="resultsList.length > 0 || resultsLoading">
|
||||||
|
<div class="results-cards">
|
||||||
|
<div
|
||||||
|
v-for="record in resultsList"
|
||||||
|
:key="record.id"
|
||||||
|
class="result-card"
|
||||||
|
:class="{
|
||||||
|
'result-card--top1': record.rank === 1,
|
||||||
|
'result-card--top2': record.rank === 2,
|
||||||
|
'result-card--top3': record.rank === 3,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="result-card__rank">
|
||||||
|
<span v-if="record.rank != null" class="rank-pill">{{ record.rank }}</span>
|
||||||
|
<span v-else class="rank-pill rank-pill--muted">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-card__body">
|
||||||
|
<div class="result-card__title-row">
|
||||||
|
<span class="result-card__name">{{ record.participantName || '-' }}</span>
|
||||||
|
<a-tag v-if="record.awardName" color="gold" class="result-card__award">
|
||||||
|
{{ record.awardName }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="result-card__meta">
|
||||||
|
<span class="result-card__meta-item">
|
||||||
|
<span class="meta-label">作品编号</span>
|
||||||
|
{{ record.workNo || '-' }}
|
||||||
|
</span>
|
||||||
|
<span class="result-card__meta-item">
|
||||||
|
<span class="meta-label">得分</span>
|
||||||
|
<span v-if="record.finalScore != null" class="score-text">
|
||||||
|
{{ Number(record.finalScore).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-muted">-</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="resultsTotal > resultsPageSize" class="results-pagination-wrap">
|
||||||
|
<a-pagination
|
||||||
|
:current="resultsPage"
|
||||||
|
:total="resultsTotal"
|
||||||
|
:page-size="resultsPageSize"
|
||||||
|
:show-size-changer="false"
|
||||||
|
show-less-items
|
||||||
|
:show-total="(t: number) => `共 ${t} 条`"
|
||||||
|
@change="onResultsPageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="empty-tab">
|
||||||
|
<a-empty description="暂无公示信息" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="notices" tab="活动公告">
|
<a-tab-pane key="notices" tab="活动公告">
|
||||||
@ -208,7 +270,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
import {
|
||||||
@ -222,6 +284,7 @@ import {
|
|||||||
publicChildrenApi,
|
publicChildrenApi,
|
||||||
type PublicActivityDetail,
|
type PublicActivityDetail,
|
||||||
type PublicActivityNotice,
|
type PublicActivityNotice,
|
||||||
|
type PublicActivityResultItem,
|
||||||
type UserWork,
|
type UserWork,
|
||||||
} from '@/api/public'
|
} from '@/api/public'
|
||||||
import WorkSelector from './components/WorkSelector.vue'
|
import WorkSelector from './components/WorkSelector.vue'
|
||||||
@ -249,6 +312,46 @@ const participantForm = ref({
|
|||||||
|
|
||||||
const formatDate = (d: string) => dayjs(d).format('YYYY-MM-DD')
|
const formatDate = (d: string) => dayjs(d).format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
const formatDateTime = (d: string) => dayjs(d).format('YYYY-MM-DD HH:mm')
|
||||||
|
|
||||||
|
/** 公示成果列表 */
|
||||||
|
const resultsLoading = ref(false)
|
||||||
|
const resultsList = ref<PublicActivityResultItem[]>([])
|
||||||
|
const resultsTotal = ref(0)
|
||||||
|
const resultsPage = ref(1)
|
||||||
|
const resultsPageSize = ref(10)
|
||||||
|
|
||||||
|
|
||||||
|
const loadPublicResults = async (page = 1) => {
|
||||||
|
if (!activity.value?.id) return
|
||||||
|
resultsLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await publicActivitiesApi.getPublishedResults(activity.value.id, {
|
||||||
|
page,
|
||||||
|
pageSize: resultsPageSize.value,
|
||||||
|
})
|
||||||
|
resultsList.value = res.list ?? []
|
||||||
|
resultsTotal.value = Number(res.total ?? 0)
|
||||||
|
resultsPage.value = Number(res.page ?? page)
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err?.response?.data?.message || '加载成果列表失败')
|
||||||
|
resultsList.value = []
|
||||||
|
resultsTotal.value = 0
|
||||||
|
} finally {
|
||||||
|
resultsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResultsPageChange = (page: number) => {
|
||||||
|
void loadPublicResults(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(activeTab, (k) => {
|
||||||
|
if (k === 'results' && activity.value?.resultState === 'published') {
|
||||||
|
void loadPublicResults(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const formatNoticeTime = (n: PublicActivityNotice) =>
|
const formatNoticeTime = (n: PublicActivityNotice) =>
|
||||||
formatDate(n.publishTime || n.createTime || '')
|
formatDate(n.publishTime || n.createTime || '')
|
||||||
|
|
||||||
@ -547,20 +650,162 @@ $primary: #6366f1;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-hint {
|
.results-panel {
|
||||||
text-align: center;
|
.results-hero {
|
||||||
padding: 40px 0;
|
text-align: center;
|
||||||
|
padding: 8px 0 16px;
|
||||||
|
|
||||||
.results-icon {
|
.results-icon {
|
||||||
font-size: 48px;
|
font-size: 40px;
|
||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-hero-text {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.results-publish-time {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-hint-line {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #faf9fe;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba($primary, 0.12);
|
||||||
|
transition: box-shadow 0.2s, border-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba($primary, 0.22);
|
||||||
|
box-shadow: 0 4px 14px rgba($primary, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--top1 {
|
||||||
|
border-color: rgba(234, 179, 8, 0.45);
|
||||||
|
background: linear-gradient(135deg, #fffbeb 0%, #faf9fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--top2 {
|
||||||
|
border-color: rgba(148, 163, 184, 0.5);
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #faf9fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--top3 {
|
||||||
|
border-color: rgba(217, 119, 6, 0.35);
|
||||||
|
background: linear-gradient(135deg, #fff7ed 0%, #faf9fe 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card__rank {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card__title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card__name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e1b4b;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card__award {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card__meta-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 28px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
background: linear-gradient(135deg, #eef2ff, #fce7f3);
|
||||||
|
color: #4f46e5;
|
||||||
|
|
||||||
|
&--muted {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-text {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user