feat: C端活动详情返回公告与附件,子女账号简化报名弹窗
Made-with: Cursor
This commit is contained in:
parent
3fa1ef95ac
commit
328533e805
@ -8,9 +8,13 @@ import com.competition.common.enums.*;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.biz.contest.entity.BizContest;
|
||||
import com.competition.modules.biz.contest.entity.BizContestAttachment;
|
||||
import com.competition.modules.biz.contest.entity.BizContestNotice;
|
||||
import com.competition.modules.biz.contest.entity.BizContestRegistration;
|
||||
import com.competition.modules.biz.contest.entity.BizContestWork;
|
||||
import com.competition.modules.biz.contest.mapper.ContestAttachmentMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestNoticeMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestWorkService;
|
||||
@ -19,6 +23,8 @@ 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.sys.entity.SysUser;
|
||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||
import com.competition.modules.user.entity.UserChild;
|
||||
import com.competition.modules.user.mapper.UserChildMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -42,6 +48,9 @@ public class PublicActivityService {
|
||||
private final UserChildMapper userChildMapper;
|
||||
private final UgcWorkMapper ugcWorkMapper;
|
||||
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||
private final ContestNoticeMapper contestNoticeMapper;
|
||||
private final ContestAttachmentMapper contestAttachmentMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
/**
|
||||
* 活动列表(公开)
|
||||
@ -102,9 +111,60 @@ public class PublicActivityService {
|
||||
result.put("workRequirement", contest.getWorkRequirement());
|
||||
result.put("resultState", contest.getResultState());
|
||||
result.put("resultPublishTime", contest.getResultPublishTime());
|
||||
|
||||
// 活动公告(仅已发布:publish_time 非空),租户范围与活动授权租户一致
|
||||
result.put("notices", listPublishedNoticesForPublic(contest));
|
||||
|
||||
// 附件(与租户端赛事详情一致,便于 C 端下载)
|
||||
LambdaQueryWrapper<BizContestAttachment> attWrapper = new LambdaQueryWrapper<>();
|
||||
attWrapper.eq(BizContestAttachment::getContestId, id);
|
||||
attWrapper.orderByAsc(BizContestAttachment::getCreateTime);
|
||||
List<BizContestAttachment> attachmentEntities = contestAttachmentMapper.selectList(attWrapper);
|
||||
List<Map<String, Object>> attachments = new ArrayList<>();
|
||||
for (BizContestAttachment a : attachmentEntities) {
|
||||
Map<String, Object> am = new LinkedHashMap<>();
|
||||
am.put("id", a.getId());
|
||||
am.put("fileName", a.getFileName());
|
||||
am.put("fileUrl", a.getFileUrl());
|
||||
am.put("fileType", a.getFileType());
|
||||
am.put("format", a.getFormat());
|
||||
am.put("size", a.getSize());
|
||||
attachments.add(am);
|
||||
}
|
||||
result.put("attachments", attachments);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公众端可见公告:已发布且属于该活动授权租户(若有);无授权租户列表时仅按 contest_id 过滤。
|
||||
*/
|
||||
private List<Map<String, Object>> listPublishedNoticesForPublic(BizContest contest) {
|
||||
Long contestId = contest.getId();
|
||||
LambdaQueryWrapper<BizContestNotice> nw = new LambdaQueryWrapper<>();
|
||||
nw.eq(BizContestNotice::getContestId, contestId);
|
||||
nw.isNotNull(BizContestNotice::getPublishTime);
|
||||
List<Integer> tenantInts = contest.getContestTenants();
|
||||
if (tenantInts != null && !tenantInts.isEmpty()) {
|
||||
List<Long> tenantIds = tenantInts.stream().map(Integer::longValue).toList();
|
||||
nw.in(BizContestNotice::getTenantId, tenantIds);
|
||||
}
|
||||
nw.orderByDesc(BizContestNotice::getPublishTime);
|
||||
List<BizContestNotice> rows = contestNoticeMapper.selectList(nw);
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
for (BizContestNotice n : rows) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("id", n.getId());
|
||||
m.put("title", n.getTitle());
|
||||
m.put("content", n.getContent());
|
||||
m.put("noticeType", n.getNoticeType());
|
||||
m.put("publishTime", n.getPublishTime());
|
||||
m.put("createTime", n.getCreateTime());
|
||||
list.add(m);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户的报名信息(包含作品提交状态)
|
||||
*/
|
||||
@ -294,6 +354,15 @@ public class PublicActivityService {
|
||||
throw new BusinessException(400, "活动未发布");
|
||||
}
|
||||
|
||||
// 子女账号仅允许以本人报名,不可代报其他子女
|
||||
SysUser currentUser = sysUserMapper.selectById(userId);
|
||||
if (currentUser != null && UserType.CHILD.getValue().equals(currentUser.getUserType())) {
|
||||
String pt = dto.getParticipantType() != null ? dto.getParticipantType() : ParticipantType.SELF.getValue();
|
||||
if (ParticipantType.CHILD.getValue().equals(pt) || dto.getChildId() != null) {
|
||||
throw new BusinessException(400, "子女账号只能以本人身份报名");
|
||||
}
|
||||
}
|
||||
|
||||
BizContestRegistration reg = new BizContestRegistration();
|
||||
reg.setContestId(contestId);
|
||||
reg.setUserId(userId);
|
||||
|
||||
@ -233,6 +233,35 @@ export interface PublicActivity {
|
||||
workRequirement: string
|
||||
}
|
||||
|
||||
/** 公众端活动详情(含公告、附件等扩展字段) */
|
||||
export interface PublicActivityNotice {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
noticeType?: string
|
||||
publishTime?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
export interface PublicActivityAttachment {
|
||||
id: number
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
fileType?: string
|
||||
format?: string
|
||||
size?: string
|
||||
}
|
||||
|
||||
export interface PublicActivityDetail extends PublicActivity {
|
||||
/** 兼容旧字段;详情正文以后端 content 为准 */
|
||||
description?: string
|
||||
notices?: PublicActivityNotice[]
|
||||
attachments?: PublicActivityAttachment[]
|
||||
ageMin?: number
|
||||
ageMax?: number
|
||||
targetCities?: string[]
|
||||
}
|
||||
|
||||
export const publicActivitiesApi = {
|
||||
list: (params?: {
|
||||
page?: number
|
||||
@ -242,7 +271,8 @@ export const publicActivitiesApi = {
|
||||
}): Promise<{ list: PublicActivity[]; total: number }> =>
|
||||
publicApi.get("/public/activities", { params }),
|
||||
|
||||
detail: (id: number) => publicApi.get(`/public/activities/${id}`),
|
||||
detail: (id: number): Promise<PublicActivityDetail> =>
|
||||
publicApi.get(`/public/activities/${id}`),
|
||||
|
||||
register: (
|
||||
id: number,
|
||||
|
||||
@ -119,7 +119,7 @@
|
||||
<div class="detail-card">
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<a-tab-pane key="info" tab="活动详情">
|
||||
<div class="rich-content" v-html="activity.description"></div>
|
||||
<div class="rich-content" v-html="activityContentHtml"></div>
|
||||
<!-- 附件 -->
|
||||
<div v-if="activity.attachments?.length" class="attachments">
|
||||
<h4>相关附件</h4>
|
||||
@ -144,7 +144,7 @@
|
||||
<div v-for="notice in activity.notices" :key="notice.id" class="notice-item">
|
||||
<h4>{{ notice.title }}</h4>
|
||||
<div class="notice-content" v-html="notice.content"></div>
|
||||
<span class="notice-time">{{ formatDate(notice.createTime) }}</span>
|
||||
<span class="notice-time">{{ formatNoticeTime(notice) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
@ -159,23 +159,28 @@
|
||||
:width="420"
|
||||
>
|
||||
<div class="register-modal">
|
||||
<p class="modal-desc">请选择参与者:</p>
|
||||
<a-radio-group v-model:value="participantForm.participantType" class="participant-options">
|
||||
<div class="participant-option">
|
||||
<a-radio value="self">我自己</a-radio>
|
||||
</div>
|
||||
<template v-if="children.length">
|
||||
<div v-for="child in children" :key="child.id" class="participant-option">
|
||||
<a-radio :value="'child_' + child.id">
|
||||
子女:{{ child.name }}
|
||||
<span class="child-detail" v-if="child.grade">({{ child.grade }})</span>
|
||||
</a-radio>
|
||||
<template v-if="isChildUser">
|
||||
<p class="modal-desc">将使用当前账号报名,确认参加本活动吗?</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="modal-desc">请选择参与者:</p>
|
||||
<a-radio-group v-model:value="participantForm.participantType" class="participant-options">
|
||||
<div class="participant-option">
|
||||
<a-radio value="self">我自己</a-radio>
|
||||
</div>
|
||||
</template>
|
||||
</a-radio-group>
|
||||
<a-button type="link" @click="$router.push('/p/mine/children')" class="add-child-link">
|
||||
+ 添加新的子女
|
||||
</a-button>
|
||||
<template v-if="children.length">
|
||||
<div v-for="child in children" :key="child.id" class="participant-option">
|
||||
<a-radio :value="'child_' + child.id">
|
||||
子女:{{ child.name }}
|
||||
<span class="child-detail" v-if="child.grade">({{ child.grade }})</span>
|
||||
</a-radio>
|
||||
</div>
|
||||
</template>
|
||||
</a-radio-group>
|
||||
<a-button type="link" @click="$router.push('/p/mine/children')" class="add-child-link">
|
||||
+ 添加新的子女
|
||||
</a-button>
|
||||
</template>
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
@ -212,13 +217,19 @@ import {
|
||||
PictureOutlined, HourglassOutlined, TrophyOutlined,
|
||||
TeamOutlined, EnvironmentOutlined, CloseCircleOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { publicActivitiesApi, publicChildrenApi, type UserWork } from '@/api/public'
|
||||
import {
|
||||
publicActivitiesApi,
|
||||
publicChildrenApi,
|
||||
type PublicActivityDetail,
|
||||
type PublicActivityNotice,
|
||||
type UserWork,
|
||||
} from '@/api/public'
|
||||
import WorkSelector from './components/WorkSelector.vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const activity = ref<any>(null)
|
||||
const activity = ref<PublicActivityDetail | null>(null)
|
||||
const activeTab = ref('info')
|
||||
const children = ref<any[]>([])
|
||||
const showRegisterModal = ref(false)
|
||||
@ -238,6 +249,27 @@ const participantForm = ref({
|
||||
|
||||
const formatDate = (d: string) => dayjs(d).format('YYYY-MM-DD')
|
||||
|
||||
const formatNoticeTime = (n: PublicActivityNotice) =>
|
||||
formatDate(n.publishTime || n.createTime || '')
|
||||
|
||||
/** 活动详情富文本:后端字段为 content */
|
||||
const activityContentHtml = computed(() => {
|
||||
const a = activity.value
|
||||
if (!a) return ''
|
||||
return a.content || a.description || ''
|
||||
})
|
||||
|
||||
/** 子女账号:直接报名,不选参与者、不添加子女 */
|
||||
const isChildUser = computed(() => {
|
||||
const raw = localStorage.getItem('public_user')
|
||||
if (!raw) return false
|
||||
try {
|
||||
return JSON.parse(raw).userType === 'child'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const isLoggedIn = computed(() => !!localStorage.getItem('public_token'))
|
||||
|
||||
// 报名是否仍在开放中
|
||||
@ -345,15 +377,20 @@ const handleRegister = async () => {
|
||||
router.push({ path: '/p/login', query: { redirect: route.fullPath } })
|
||||
return
|
||||
}
|
||||
if (!activity.value) return
|
||||
|
||||
registering.value = true
|
||||
try {
|
||||
const val = participantForm.value.participantType
|
||||
const isChild = val.startsWith('child_')
|
||||
await publicActivitiesApi.register(activity.value.id, {
|
||||
participantType: isChild ? 'child' : 'self',
|
||||
childId: isChild ? parseInt(val.replace('child_', '')) : undefined,
|
||||
})
|
||||
if (isChildUser.value) {
|
||||
await publicActivitiesApi.register(activity.value.id, { participantType: 'self' })
|
||||
} else {
|
||||
const val = participantForm.value.participantType
|
||||
const isChild = val.startsWith('child_')
|
||||
await publicActivitiesApi.register(activity.value.id, {
|
||||
participantType: isChild ? 'child' : 'self',
|
||||
childId: isChild ? parseInt(val.replace('child_', '')) : undefined,
|
||||
})
|
||||
}
|
||||
message.success('报名成功!')
|
||||
showRegisterModal.value = false
|
||||
hasRegistered.value = true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user