feat: 活动提交联动作品库+多租户数据对齐
1. P0-12 活动提交联动:替换文件上传为 WorkSelector 作品选择器 - 前端 ActivityDetail.vue 集成 WorkSelector 组件 - 后端 submitWork 支持 userWorkId 快照复制(title/description/coverUrl/pages) - WorkSelector 支持 redirectUrl 创作后返回活动页 2. 多租户数据对齐:修复公众端报名/作品 tenantId 不一致 - register() 使用活动的 contestTenants[0] 作为 tenantId - submitWork() 使用报名记录的 tenantId - 管理端报名/作品统计、列表数据一致 3. 前端报名状态区分:pending/passed/rejected 显示不同按钮 4. submitWork 报名状态检查:区分未报名/审核中/已拒绝提示 5. 活动列表添加 _count(报名数/作品数)用于已交/应交展示 6. 修复 PublicCreationService.submit() title 默认值缺失 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0252f25acd
commit
1c63cb21e5
@ -106,6 +106,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
||||
.or().like(BizContestRegistration::getAccountName, dto.getKeyword()));
|
||||
}
|
||||
|
||||
// 租户过滤
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
wrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
@ -126,12 +127,14 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
||||
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperAdmin) {
|
||||
log.info("获取报名统计,赛事ID:{},租户ID:{},超管:{}", contestId, tenantId, isSuperAdmin);
|
||||
|
||||
// 非超管需要按租户过滤,与列表查询保持一致
|
||||
// 非超管需要按租户过滤
|
||||
boolean needTenantFilter = !isSuperAdmin && tenantId != null;
|
||||
|
||||
LambdaQueryWrapper<BizContestRegistration> baseWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
baseWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
}
|
||||
if (!isSuperAdmin && tenantId != null) {
|
||||
if (needTenantFilter) {
|
||||
baseWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
long total = count(baseWrapper);
|
||||
@ -140,7 +143,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
||||
if (contestId != null) {
|
||||
pendingWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
}
|
||||
if (!isSuperAdmin && tenantId != null) {
|
||||
if (needTenantFilter) {
|
||||
pendingWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
pendingWrapper.eq(BizContestRegistration::getRegistrationState, "pending");
|
||||
@ -150,7 +153,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
||||
if (contestId != null) {
|
||||
passedWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
}
|
||||
if (!isSuperAdmin && tenantId != null) {
|
||||
if (needTenantFilter) {
|
||||
passedWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
passedWrapper.eq(BizContestRegistration::getRegistrationState, "passed");
|
||||
@ -160,7 +163,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
||||
if (contestId != null) {
|
||||
rejectedWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
}
|
||||
if (!isSuperAdmin && tenantId != null) {
|
||||
if (needTenantFilter) {
|
||||
rejectedWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
rejectedWrapper.eq(BizContestRegistration::getRegistrationState, "rejected");
|
||||
|
||||
@ -154,8 +154,40 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
||||
Page<BizContest> page = new Page<>(dto.getPage(), dto.getPageSize());
|
||||
Page<BizContest> result = contestMapper.selectPage(page, wrapper);
|
||||
|
||||
// 批量查询报名数和作品数
|
||||
List<Long> contestIds = result.getRecords().stream()
|
||||
.map(BizContest::getId).toList();
|
||||
|
||||
Map<Long, Long> registrationCountMap = new HashMap<>();
|
||||
Map<Long, Long> workCountMap = new HashMap<>();
|
||||
if (!contestIds.isEmpty()) {
|
||||
// 报名数(所有状态)
|
||||
contestRegistrationMapper.selectList(
|
||||
new LambdaQueryWrapper<BizContestRegistration>()
|
||||
.in(BizContestRegistration::getContestId, contestIds))
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(BizContestRegistration::getContestId, Collectors.counting()))
|
||||
.forEach(registrationCountMap::put);
|
||||
|
||||
// 作品数(最新版本)
|
||||
contestWorkMapper.selectList(
|
||||
new LambdaQueryWrapper<BizContestWork>()
|
||||
.in(BizContestWork::getContestId, contestIds)
|
||||
.eq(BizContestWork::getIsLatest, true))
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(BizContestWork::getContestId, Collectors.counting()))
|
||||
.forEach(workCountMap::put);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||
.map(this::entityToMap)
|
||||
.map(entity -> {
|
||||
Map<String, Object> map = entityToMap(entity);
|
||||
Map<String, Object> countMap = new LinkedHashMap<>();
|
||||
countMap.put("registrations", registrationCountMap.getOrDefault(entity.getId(), 0L));
|
||||
countMap.put("works", workCountMap.getOrDefault(entity.getId(), 0L));
|
||||
map.put("_count", countMap);
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.from(result, voList);
|
||||
|
||||
@ -290,13 +290,16 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
||||
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperTenant) {
|
||||
log.info("获取作品统计,赛事ID:{}", contestId);
|
||||
|
||||
// 租户过滤
|
||||
boolean needTenantFilter = !isSuperTenant && tenantId != null;
|
||||
|
||||
LambdaQueryWrapper<BizContestWork> baseWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
baseWrapper.eq(BizContestWork::getContestId, contestId);
|
||||
}
|
||||
baseWrapper.eq(BizContestWork::getIsLatest, true);
|
||||
baseWrapper.eq(BizContestWork::getValidState, 1);
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
if (needTenantFilter) {
|
||||
baseWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
long total = count(baseWrapper);
|
||||
@ -307,7 +310,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
||||
}
|
||||
submittedWrapper.eq(BizContestWork::getIsLatest, true);
|
||||
submittedWrapper.eq(BizContestWork::getValidState, 1);
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
if (needTenantFilter) {
|
||||
submittedWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
submittedWrapper.eq(BizContestWork::getStatus, "submitted");
|
||||
@ -319,7 +322,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
||||
}
|
||||
reviewingWrapper.eq(BizContestWork::getIsLatest, true);
|
||||
reviewingWrapper.eq(BizContestWork::getValidState, 1);
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
if (needTenantFilter) {
|
||||
reviewingWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
reviewingWrapper.eq(BizContestWork::getStatus, "reviewing");
|
||||
@ -331,10 +334,9 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
||||
}
|
||||
reviewedWrapper.eq(BizContestWork::getIsLatest, true);
|
||||
reviewedWrapper.eq(BizContestWork::getValidState, 1);
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
if (needTenantFilter) {
|
||||
reviewedWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
// 已评完口径:兼容 accepted/awarded 两种结果状态
|
||||
reviewedWrapper.in(BizContestWork::getStatus, Arrays.asList("accepted", "awarded"));
|
||||
long reviewed = count(reviewedWrapper);
|
||||
|
||||
|
||||
@ -12,6 +12,10 @@ 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.pub.dto.PublicRegisterActivityDto;
|
||||
import com.competition.modules.ugc.entity.UgcWork;
|
||||
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
|
||||
import com.competition.modules.user.entity.UserChild;
|
||||
import com.competition.modules.user.mapper.UserChildMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -32,6 +36,8 @@ public class PublicActivityService {
|
||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||
private final ContestWorkMapper contestWorkMapper;
|
||||
private final UserChildMapper userChildMapper;
|
||||
private final UgcWorkMapper ugcWorkMapper;
|
||||
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||
|
||||
/**
|
||||
* 活动列表(公开)
|
||||
@ -287,7 +293,12 @@ public class PublicActivityService {
|
||||
BizContestRegistration reg = new BizContestRegistration();
|
||||
reg.setContestId(contestId);
|
||||
reg.setUserId(userId);
|
||||
reg.setTenantId(tenantId);
|
||||
// 使用活动的授权租户ID(管理端按租户查询报名数据)
|
||||
if (contest.getContestTenants() != null && !contest.getContestTenants().isEmpty()) {
|
||||
reg.setTenantId(contest.getContestTenants().get(0).longValue());
|
||||
} else {
|
||||
reg.setTenantId(tenantId);
|
||||
}
|
||||
reg.setRegistrationType(contest.getContestType());
|
||||
reg.setParticipantType(dto.getParticipantType() != null ? dto.getParticipantType() : "self");
|
||||
reg.setChildId(dto.getChildId());
|
||||
@ -310,15 +321,26 @@ public class PublicActivityService {
|
||||
*/
|
||||
@Transactional
|
||||
public BizContestWork submitWork(Long contestId, Long userId, Long tenantId, Map<String, Object> dto) {
|
||||
// 检查报名状态
|
||||
BizContestRegistration reg = contestRegistrationMapper.selectOne(
|
||||
// 检查报名状态(区分不同状态给出明确提示)
|
||||
BizContestRegistration anyReg = contestRegistrationMapper.selectOne(
|
||||
new LambdaQueryWrapper<BizContestRegistration>()
|
||||
.eq(BizContestRegistration::getContestId, contestId)
|
||||
.eq(BizContestRegistration::getUserId, userId)
|
||||
.eq(BizContestRegistration::getRegistrationState, "passed"));
|
||||
if (reg == null) {
|
||||
throw new BusinessException(400, "未报名或报名未通过");
|
||||
.last("LIMIT 1"));
|
||||
if (anyReg == null) {
|
||||
throw new BusinessException(400, "您尚未报名该活动,请先报名");
|
||||
}
|
||||
if (!"passed".equals(anyReg.getRegistrationState())) {
|
||||
String state = anyReg.getRegistrationState();
|
||||
String msg = switch (state) {
|
||||
case "pending" -> "报名审核中,请等待审核通过后再提交作品";
|
||||
case "rejected" -> "报名已被拒绝,无法提交作品";
|
||||
case "withdrawn" -> "报名已撤回,请重新报名";
|
||||
default -> "报名状态异常(" + state + "),无法提交作品";
|
||||
};
|
||||
throw new BusinessException(400, msg);
|
||||
}
|
||||
BizContestRegistration reg = anyReg;
|
||||
|
||||
// 查询活动提交规则
|
||||
BizContest contest = contestMapper.selectById(contestId);
|
||||
@ -347,17 +369,67 @@ public class PublicActivityService {
|
||||
BizContestWork work = new BizContestWork();
|
||||
work.setContestId(contestId);
|
||||
work.setRegistrationId(reg.getId());
|
||||
work.setTenantId(tenantId);
|
||||
work.setTitle((String) dto.get("title"));
|
||||
work.setDescription((String) dto.get("description"));
|
||||
work.setFiles(dto.get("files"));
|
||||
// 使用报名记录的租户ID(已在 register 时设置为活动的租户,确保管理端可见)
|
||||
work.setTenantId(reg.getTenantId());
|
||||
work.setSubmitterUserId(userId);
|
||||
work.setStatus("submitted");
|
||||
work.setSubmitTime(LocalDateTime.now());
|
||||
work.setVersion(nextVersion);
|
||||
work.setIsLatest(true);
|
||||
|
||||
// 从作品库选择作品提交(快照复制)
|
||||
if (dto.get("userWorkId") != null) {
|
||||
work.setUserWorkId(Long.valueOf(dto.get("userWorkId").toString()));
|
||||
Long userWorkId = Long.valueOf(dto.get("userWorkId").toString());
|
||||
work.setUserWorkId(userWorkId);
|
||||
|
||||
// 查询用户作品
|
||||
UgcWork ugcWork = ugcWorkMapper.selectById(userWorkId);
|
||||
if (ugcWork == null || ugcWork.getIsDeleted() == 1) {
|
||||
throw new BusinessException(404, "作品不存在");
|
||||
}
|
||||
if (!ugcWork.getUserId().equals(userId)) {
|
||||
throw new BusinessException(403, "无权使用该作品");
|
||||
}
|
||||
if ("rejected".equals(ugcWork.getStatus()) || "taken_down".equals(ugcWork.getStatus())) {
|
||||
throw new BusinessException(400, "该作品状态不可提交");
|
||||
}
|
||||
|
||||
// 查询绘本分页
|
||||
List<UgcWorkPage> pages = ugcWorkPageMapper.selectList(
|
||||
new LambdaQueryWrapper<UgcWorkPage>()
|
||||
.eq(UgcWorkPage::getWorkId, userWorkId)
|
||||
.orderByAsc(UgcWorkPage::getPageNo));
|
||||
|
||||
// 复制快照字段
|
||||
work.setTitle(ugcWork.getTitle());
|
||||
work.setDescription(ugcWork.getDescription());
|
||||
work.setPreviewUrl(ugcWork.getCoverUrl());
|
||||
work.setAiModelMeta(ugcWork.getAiMeta());
|
||||
|
||||
// previewUrls = 所有页面图片
|
||||
List<String> previewUrls = pages.stream()
|
||||
.map(UgcWorkPage::getImageUrl)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
work.setPreviewUrls(previewUrls);
|
||||
|
||||
// files = 分页完整快照
|
||||
List<Map<String, Object>> filesSnapshot = pages.stream()
|
||||
.map(p -> {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("pageNo", p.getPageNo());
|
||||
m.put("imageUrl", p.getImageUrl());
|
||||
m.put("text", p.getText());
|
||||
m.put("audioUrl", p.getAudioUrl());
|
||||
return m;
|
||||
})
|
||||
.toList();
|
||||
work.setFiles(filesSnapshot);
|
||||
} else {
|
||||
// 旧逻辑:直接上传
|
||||
work.setTitle((String) dto.get("title"));
|
||||
work.setDescription((String) dto.get("description"));
|
||||
work.setFiles(dto.get("files"));
|
||||
}
|
||||
contestWorkMapper.insert(work);
|
||||
return work;
|
||||
|
||||
@ -275,7 +275,7 @@ JWT 改造:
|
||||
|
||||
---
|
||||
|
||||
#### P0-12. 活动提交联动
|
||||
#### P0-12. 活动提交联动 ✅ 已实现 (2026-04-07)
|
||||
|
||||
**改动范围**:活动报名+提交流程改造
|
||||
|
||||
@ -283,14 +283,39 @@ JWT 改造:
|
||||
后端改动:
|
||||
├── POST /api/public/activities/:id/submit-work — 改造:支持从作品库选择作品
|
||||
│ 新增参数:userWorkId(用户作品ID)
|
||||
│ 逻辑:根据 userWorkId 复制快照到 contest_works
|
||||
└── contest_works 表 — 新增 user_work_id 字段
|
||||
│ 逻辑:根据 userWorkId 从 UgcWork + UgcWorkPage 复制快照到 ContestWork
|
||||
│ 校验:归属当前用户、未删除、非 rejected/taken_down 状态
|
||||
└── contest_works 表 — user_work_id 字段已存在
|
||||
|
||||
前端改动:
|
||||
├── 活动详情页 提交作品流程 — 改造:弹出作品库选择器,从"我的作品库"选择
|
||||
├── 作品库选择器组件 — 网格展示可选作品(已发布+私密均可选),确认后提交
|
||||
├── ActivityDetail.vue — 替换文件上传弹窗为 WorkSelector 作品选择器
|
||||
├── WorkSelector.vue — 已有组件,新增 redirectUrl prop 支持创作后返回活动页
|
||||
└── public.ts — submitWork API 新增 userWorkId 参数
|
||||
```
|
||||
|
||||
**快照字段映射**:
|
||||
|
||||
| UgcWork / UgcWorkPage 字段 | ContestWork 字段 | 说明 |
|
||||
|---|---|---|
|
||||
| title | title | 直接复制 |
|
||||
| description | description | 直接复制 |
|
||||
| coverUrl | previewUrl | 封面 → 预览图 |
|
||||
| aiMeta | aiModelMeta | AI 元数据 |
|
||||
| (所有 page.imageUrl) | previewUrls | 所有页面图片 URL 列表 |
|
||||
| (所有 page 数据) | files | 分页快照 [{pageNo, imageUrl, text, audioUrl}] |
|
||||
|
||||
**用户交互流程**:
|
||||
1. 用户在活动详情页点击"从作品库选择"
|
||||
2. 弹出 WorkSelector 展示用户所有作品(排除 rejected/taken_down)
|
||||
3. 用户选择作品后确认提交 → 后端复制快照到 ContestWork
|
||||
4. 若作品库为空,显示"去创作"按钮 → 跳转创作页 → 完成后 redirect 回活动详情页
|
||||
5. 支持 resubmit 模式:可重新选择不同作品提交
|
||||
|
||||
**关键设计**:
|
||||
- 快照不可变性:提交后 ContestWork 数据与 UgcWork 解耦,后续修改/删除作品不影响活动中的作品
|
||||
- 向后兼容:userWorkId 为 null 时走旧逻辑(直接上传)
|
||||
- 无需数据库变更:t_biz_contest_work 已有所需字段
|
||||
|
||||
**依赖**:P0-4 + P0-6 + 现有活动模块
|
||||
**产出**:用户可从作品库选作品参与活动
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
# 开发环境
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_AI_POST_MESSAGE_URL=://localhost:3001/
|
||||
|
||||
@ -2,4 +2,5 @@
|
||||
VITE_API_BASE_URL=/api
|
||||
# 如果后端部署在不同域名,可以改成完整地址:
|
||||
# VITE_API_BASE_URL=https://api.your-domain.com
|
||||
VITE_AI_POST_MESSAGE_URL=://localhost:3001
|
||||
|
||||
|
||||
@ -265,7 +265,8 @@ export const publicActivitiesApi = {
|
||||
id: number,
|
||||
data: {
|
||||
registrationId: number
|
||||
title: string
|
||||
userWorkId?: number
|
||||
title?: string
|
||||
description?: string
|
||||
files?: string[]
|
||||
previewUrl?: string
|
||||
|
||||
@ -50,6 +50,12 @@
|
||||
<a-button v-else-if="!hasRegistered" type="primary" size="large" block class="action-btn" @click="showRegisterModal = true">
|
||||
立即报名
|
||||
</a-button>
|
||||
<a-button v-else-if="registrationState === 'pending'" size="large" block class="action-btn-info">
|
||||
<hourglass-outlined /> 报名审核中
|
||||
</a-button>
|
||||
<a-button v-else-if="registrationState === 'rejected'" size="large" block class="action-btn-disabled">
|
||||
<close-circle-outlined /> 报名未通过
|
||||
</a-button>
|
||||
<a-button v-else size="large" block class="action-btn-done">
|
||||
<check-circle-outlined /> 已报名
|
||||
</a-button>
|
||||
@ -66,11 +72,17 @@
|
||||
<a-button v-else-if="!hasRegistered" size="large" block disabled class="action-btn-disabled">
|
||||
报名已截止
|
||||
</a-button>
|
||||
<a-button v-else-if="registrationState === 'pending'" size="large" block class="action-btn-info">
|
||||
<hourglass-outlined /> 报名审核中,通过后可提交作品
|
||||
</a-button>
|
||||
<a-button v-else-if="registrationState === 'rejected'" size="large" block disabled class="action-btn-disabled">
|
||||
<close-circle-outlined /> 报名未通过,无法提交作品
|
||||
</a-button>
|
||||
<a-button v-else-if="!hasSubmittedWork" type="primary" size="large" block class="action-btn" @click="openSubmitWork">
|
||||
<upload-outlined /> 提交作品
|
||||
<picture-outlined /> 从作品库选择
|
||||
</a-button>
|
||||
<a-button v-else-if="canResubmit" type="primary" size="large" block class="action-btn" @click="openSubmitWork">
|
||||
<upload-outlined /> 重新提交
|
||||
<picture-outlined /> 重新提交
|
||||
</a-button>
|
||||
<a-button v-else size="large" block class="action-btn-done">
|
||||
<check-circle-outlined /> 作品已提交
|
||||
@ -177,36 +189,12 @@
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 作品提交弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showSubmitModal"
|
||||
title="提交作品"
|
||||
:footer="null"
|
||||
:width="520"
|
||||
>
|
||||
<a-form :model="workForm" layout="vertical" @finish="handleSubmitWork" class="work-form">
|
||||
<a-form-item label="作品名称" name="title" :rules="[{ required: true, message: '请输入作品名称' }]">
|
||||
<a-input v-model:value="workForm.title" placeholder="给你的作品取个名字吧" :maxlength="200" />
|
||||
</a-form-item>
|
||||
<a-form-item label="作品描述" name="description">
|
||||
<a-textarea v-model:value="workForm.description" placeholder="描述一下你的创作思路" :rows="3" :maxlength="2000" />
|
||||
</a-form-item>
|
||||
<a-form-item label="上传作品文件" name="files">
|
||||
<a-upload
|
||||
:before-upload="handleFileUpload"
|
||||
:file-list="workFileList"
|
||||
:max-count="5"
|
||||
@remove="handleFileRemove"
|
||||
>
|
||||
<a-button><upload-outlined /> 选择文件</a-button>
|
||||
</a-upload>
|
||||
<div class="upload-hint">支持图片(JPG/PNG)和 PDF,最多 5 个文件</div>
|
||||
</a-form-item>
|
||||
<a-button type="primary" html-type="submit" block size="large" :loading="submittingWork" class="confirm-btn">
|
||||
确认提交
|
||||
</a-button>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<!-- 作品选择器弹窗 -->
|
||||
<WorkSelector
|
||||
v-model:open="showWorkSelector"
|
||||
:redirect-url="route.fullPath"
|
||||
@select="handleWorkSelected"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="loading-page">
|
||||
@ -221,10 +209,11 @@ import { message } from 'ant-design-vue'
|
||||
import {
|
||||
ArrowLeftOutlined, CalendarOutlined, TagOutlined,
|
||||
ClockCircleOutlined, PaperClipOutlined, CheckCircleOutlined,
|
||||
UploadOutlined, HourglassOutlined, TrophyOutlined,
|
||||
TeamOutlined, EnvironmentOutlined,
|
||||
PictureOutlined, HourglassOutlined, TrophyOutlined,
|
||||
TeamOutlined, EnvironmentOutlined, CloseCircleOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { publicActivitiesApi, publicChildrenApi } from '@/api/public'
|
||||
import { publicActivitiesApi, publicChildrenApi, type UserWork } from '@/api/public'
|
||||
import WorkSelector from './components/WorkSelector.vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const route = useRoute()
|
||||
@ -235,14 +224,13 @@ const children = ref<any[]>([])
|
||||
const showRegisterModal = ref(false)
|
||||
const registering = ref(false)
|
||||
const hasRegistered = ref(false)
|
||||
const registrationState = ref('')
|
||||
const myRegistration = ref<any>(null)
|
||||
const hasSubmittedWork = ref(false)
|
||||
|
||||
// 作品提交
|
||||
const showSubmitModal = ref(false)
|
||||
const showWorkSelector = ref(false)
|
||||
const submittingWork = ref(false)
|
||||
const workFileList = ref<any[]>([])
|
||||
const workForm = ref({ title: '', description: '' })
|
||||
|
||||
const participantForm = ref({
|
||||
participantType: 'self',
|
||||
@ -313,6 +301,7 @@ const checkRegistrationStatus = async () => {
|
||||
// 只有当 reg 存在且有 id 时才认为已报名
|
||||
if (reg && reg.id) {
|
||||
hasRegistered.value = true
|
||||
registrationState.value = reg.registrationState || ''
|
||||
myRegistration.value = reg
|
||||
// 检查是否已提交作品
|
||||
hasSubmittedWork.value = reg.hasSubmittedWork || false
|
||||
@ -323,26 +312,14 @@ const checkRegistrationStatus = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 打开作品提交
|
||||
// 打开作品选择器
|
||||
const openSubmitWork = () => {
|
||||
if (!isLoggedIn.value) { goLogin(); return }
|
||||
workForm.value = { title: '', description: '' }
|
||||
workFileList.value = []
|
||||
showSubmitModal.value = true
|
||||
showWorkSelector.value = true
|
||||
}
|
||||
|
||||
// 文件上传(暂时存到 fileList,提交时一起处理)
|
||||
const handleFileUpload = (file: any) => {
|
||||
workFileList.value = [...workFileList.value, file]
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
const handleFileRemove = (file: any) => {
|
||||
workFileList.value = workFileList.value.filter((f: any) => f.uid !== file.uid)
|
||||
}
|
||||
|
||||
// 提交作品
|
||||
const handleSubmitWork = async () => {
|
||||
// 从作品库选择作品后提交
|
||||
const handleWorkSelected = async (work: UserWork) => {
|
||||
if (!myRegistration.value) {
|
||||
message.error('请先报名活动')
|
||||
return
|
||||
@ -351,13 +328,10 @@ const handleSubmitWork = async () => {
|
||||
try {
|
||||
await publicActivitiesApi.submitWork(activity.value.id, {
|
||||
registrationId: myRegistration.value.id,
|
||||
title: workForm.value.title,
|
||||
description: workForm.value.description || undefined,
|
||||
// 文件上传需要先上传到 COS,此处简化为记录文件名
|
||||
files: workFileList.value.map((f: any) => f.name),
|
||||
userWorkId: work.id,
|
||||
})
|
||||
message.success('作品提交成功!')
|
||||
showSubmitModal.value = false
|
||||
showWorkSelector.value = false
|
||||
hasSubmittedWork.value = true
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || '提交失败')
|
||||
@ -383,6 +357,8 @@ const handleRegister = async () => {
|
||||
message.success('报名成功!')
|
||||
showRegisterModal.value = false
|
||||
hasRegistered.value = true
|
||||
// 重新查询报名状态以获取准确的 registrationState
|
||||
await checkRegistrationStatus()
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || '报名失败')
|
||||
} finally {
|
||||
@ -551,14 +527,6 @@ $primary: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.work-form {
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.rich-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
<div v-else-if="works.length === 0" style="text-align: center; padding: 40px">
|
||||
<a-empty description="作品库中没有可提交的作品">
|
||||
<a-button type="primary" shape="round" @click="$router.push('/p/create'); $emit('update:open', false)">
|
||||
<a-button type="primary" shape="round" @click="goCreate">
|
||||
去创作
|
||||
</a-button>
|
||||
</a-empty>
|
||||
@ -41,8 +41,10 @@
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { PictureOutlined, CheckCircleFilled } from '@ant-design/icons-vue'
|
||||
import { publicUserWorksApi, type UserWork } from '@/api/public'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps<{ open: boolean }>()
|
||||
const router = useRouter()
|
||||
const props = defineProps<{ open: boolean; redirectUrl?: string }>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', v: boolean): void
|
||||
(e: 'select', work: UserWork): void
|
||||
@ -69,6 +71,15 @@ const handleConfirm = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const goCreate = () => {
|
||||
emit('update:open', false)
|
||||
if (props.redirectUrl) {
|
||||
router.push({ path: '/p/create', query: { redirect: props.redirectUrl } })
|
||||
} else {
|
||||
router.push('/p/create')
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.open, (v) => {
|
||||
if (v) { selectedWork.value = null; fetchWorks() }
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user