Merge remote-tracking branch 'origin/master_develop' into master_develop

This commit is contained in:
En 2026-04-15 15:23:05 +08:00
commit ee9a519d57
8 changed files with 183 additions and 132 deletions

View File

@ -10,10 +10,12 @@ import com.lesingle.common.result.PageResult;
import com.lesingle.modules.biz.contest.entity.BizContest;
import com.lesingle.modules.biz.contest.entity.BizContestAttachment;
import com.lesingle.modules.biz.contest.entity.BizContestNotice;
import com.lesingle.modules.biz.contest.entity.BizContestNoticeAttachment;
import com.lesingle.modules.biz.contest.entity.BizContestRegistration;
import com.lesingle.modules.biz.contest.entity.BizContestWork;
import com.lesingle.modules.biz.contest.mapper.ContestAttachmentMapper;
import com.lesingle.modules.biz.contest.mapper.ContestMapper;
import com.lesingle.modules.biz.contest.mapper.ContestNoticeAttachmentMapper;
import com.lesingle.modules.biz.contest.mapper.ContestNoticeMapper;
import com.lesingle.modules.biz.contest.mapper.ContestRegistrationMapper;
import com.lesingle.modules.biz.contest.mapper.ContestWorkMapper;
@ -50,6 +52,7 @@ public class PublicActivityService {
private final UgcWorkMapper ugcWorkMapper;
private final UgcWorkPageMapper ugcWorkPageMapper;
private final ContestNoticeMapper contestNoticeMapper;
private final ContestNoticeAttachmentMapper contestNoticeAttachmentMapper;
private final ContestAttachmentMapper contestAttachmentMapper;
private final SysUserMapper sysUserMapper;
@ -163,6 +166,31 @@ public class PublicActivityService {
m.put("createTime", n.getCreateTime());
list.add(m);
}
if (!rows.isEmpty()) {
List<Long> noticeIds = rows.stream().map(BizContestNotice::getId).filter(Objects::nonNull).toList();
LambdaQueryWrapper<BizContestNoticeAttachment> attw = new LambdaQueryWrapper<>();
attw.in(BizContestNoticeAttachment::getNoticeId, noticeIds);
attw.orderByAsc(BizContestNoticeAttachment::getCreateTime);
List<BizContestNoticeAttachment> attRows = contestNoticeAttachmentMapper.selectList(attw);
Map<Long, List<BizContestNoticeAttachment>> byNotice = attRows.stream()
.collect(Collectors.groupingBy(BizContestNoticeAttachment::getNoticeId));
for (Map<String, Object> m : list) {
Long nid = (Long) m.get("id");
List<BizContestNoticeAttachment> nas = byNotice.getOrDefault(nid, Collections.emptyList());
List<Map<String, Object>> attList = new ArrayList<>();
for (BizContestNoticeAttachment a : nas) {
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());
attList.add(am);
}
m.put("attachments", attList);
}
}
return list;
}

View File

@ -289,6 +289,15 @@ export interface PublicActivity {
workRequirement: string;
}
export interface PublicActivityAttachment {
id: number;
fileName: string;
fileUrl: string;
fileType?: string;
format?: string;
size?: string;
}
/** 公众端活动详情(含公告、附件等扩展字段) */
export interface PublicActivityNotice {
id: number;
@ -297,15 +306,8 @@ export interface PublicActivityNotice {
noticeType?: string;
publishTime?: string;
createTime?: string;
}
export interface PublicActivityAttachment {
id: number;
fileName: string;
fileUrl: string;
fileType?: string;
format?: string;
size?: string;
/** 公告附件(公众活动详情接口填充,与活动附件字段一致) */
attachments?: PublicActivityAttachment[];
}
export interface PublicActivityDetail extends PublicActivity {

View File

@ -4,15 +4,12 @@
<!-- 顶部海报区域 -->
<div class="hero-section">
<!-- 背景图 -->
<div
class="hero-bg"
:style="{
backgroundImage:
contest?.posterUrl || contest?.coverUrl
? `url(${contest.posterUrl || contest.coverUrl})`
: undefined,
}"
>
<div class="hero-bg" :style="{
backgroundImage:
contest?.posterUrl || contest?.coverUrl
? `url(${contest.posterUrl || contest.coverUrl})`
: undefined,
}">
<div class="hero-overlay"></div>
</div>
@ -46,43 +43,29 @@
<div class="hero-meta">
<div class="meta-item">
<CalendarOutlined />
<span
>活动时间{{ formatDate(contest?.startTime) }} ~
{{ formatDate(contest?.endTime) }}</span
>
<span>活动时间{{ formatDate(contest?.startTime) }} ~
{{ formatDate(contest?.endTime) }}</span>
</div>
<div class="meta-item">
<ClockCircleOutlined />
<span
>报名时间{{ formatDate(contest?.registerStartTime) }} ~
{{ formatDate(contest?.registerEndTime) }}</span
>
<span>报名时间{{ formatDate(contest?.registerStartTime) }} ~
{{ formatDate(contest?.registerEndTime) }}</span>
</div>
</div>
<!-- 报名按钮 -->
<div class="hero-actions">
<button
v-if="isTeacher && isRegistering && !hasRegistered"
class="action-btn primary"
@click="handleRegister"
>
<button v-if="isTeacher && isRegistering && !hasRegistered" class="action-btn primary"
@click="handleRegister">
<FormOutlined />
立即报名
</button>
<button
v-else-if="isTeacher && hasRegistered && canViewRegistration"
class="action-btn secondary"
@click="handleViewRegistration"
>
<button v-else-if="isTeacher && hasRegistered && canViewRegistration" class="action-btn secondary"
@click="handleViewRegistration">
<EyeOutlined />
查看报名
</button>
<button
v-else-if="isTeacher"
class="action-btn disabled"
disabled
>
<button v-else-if="isTeacher" class="action-btn disabled" disabled>
<FormOutlined />
{{ isRegistering ? "立即报名" : "报名已截止" }}
</button>
@ -98,30 +81,18 @@
<div class="tab-section">
<div class="tab-container">
<div class="custom-tabs">
<div
class="tab-item"
:class="{ active: activeTab === 'info' }"
@click="handleTabChange('info')"
>
<div class="tab-item" :class="{ active: activeTab === 'info' }" @click="handleTabChange('info')">
<FileTextOutlined />
<span>活动信息</span>
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'notices' }"
@click="handleTabChange('notices')"
>
<div class="tab-item" :class="{ active: activeTab === 'notices' }" @click="handleTabChange('notices')">
<BellOutlined />
<span>通知公告</span>
<span v-if="notices.length > 0" class="badge">{{
notices.length
}}</span>
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'results' }"
@click="handleTabChange('results')"
>
<div class="tab-item" :class="{ active: activeTab === 'results' }" @click="handleTabChange('results')">
<TrophyOutlined />
<span>活动结果</span>
</div>
@ -144,11 +115,7 @@
<h2>竞赛详情</h2>
</div>
<div class="card-body">
<div
v-if="contest.content"
class="rich-content"
v-html="contest.content"
></div>
<div v-if="contest.content" class="rich-content" v-html="contest.content"></div>
<a-empty v-else description="暂无详情内容" />
</div>
</div>
@ -165,18 +132,11 @@
<div v-if="noticesLoading" class="loading-placeholder">
<a-spin />
</div>
<div
v-else-if="notices.length === 0"
class="empty-placeholder"
>
<div v-else-if="notices.length === 0" class="empty-placeholder">
<a-empty description="暂无公告" />
</div>
<div v-else class="notice-list">
<div
v-for="item in notices"
:key="item.id"
class="notice-item"
>
<div v-for="item in notices" :key="item.id" class="notice-item">
<div class="notice-header">
<span class="notice-title">{{ item.title }}</span>
<span class="notice-type" :class="item.noticeType">
@ -184,6 +144,15 @@
</span>
</div>
<div class="notice-content">{{ item.content }}</div>
<div v-if="item.attachments?.length" class="notice-attachments-block">
<div class="notice-attachments-title">公告附件</div>
<div v-for="na in item.attachments" :key="na.id" class="notice-att-link">
<a :href="na.fileUrl" target="_blank" rel="noopener noreferrer">
<PaperClipOutlined />
{{ na.fileName }}
</a>
</div>
</div>
<div class="notice-time">
<ClockCircleOutlined />
{{ formatDateTime(item.publishTime) }}
@ -204,19 +173,11 @@
<div class="card-body">
<div v-if="contest.resultState === 'published'">
<a-spin :spinning="resultsLoading">
<a-table
:columns="resultColumns"
:data-source="results"
:pagination="resultsPagination"
row-key="id"
@change="handleResultsTableChange"
>
<a-table :columns="resultColumns" :data-source="results" :pagination="resultsPagination"
row-key="id" @change="handleResultsTableChange">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'rank'">
<div
class="rank-badge"
:class="getRankClass(record.rank)"
>
<div class="rank-badge" :class="getRankClass(record.rank)">
{{ record.rank || "-" }}
</div>
</template>
@ -228,11 +189,7 @@
}}
</template>
<template v-else-if="column.key === 'award'">
<span
v-if="record.awardName"
class="award-tag"
:class="getAwardClass(record.awardName)"
>
<span v-if="record.awardName" class="award-tag" :class="getAwardClass(record.awardName)">
{{ record.awardName }}
</span>
<span v-else>-</span>
@ -265,20 +222,14 @@
{{ getVisibilityText(contest?.visibility) }}
</div>
</div>
<div
v-if="contest?.visibility === 'designated' && contest?.contestTenantInfos?.length"
class="sidebar-item"
>
<div v-if="contest?.visibility === 'designated' && contest?.contestTenantInfos?.length"
class="sidebar-item">
<div class="item-label">
<TeamOutlined />
开放机构
</div>
<div class="item-value">
<div
v-for="tenant in contest.contestTenantInfos"
:key="tenant.id"
class="org-name"
>
<div v-for="tenant in contest.contestTenantInfos" :key="tenant.id" class="org-name">
{{ tenant.name }}{{ tenant.code }}
</div>
</div>
@ -301,6 +252,20 @@
{{ formatDateTime(contest?.createTime) }}
</div>
</div>
<div v-if="contest.attachments?.length" class="sidebar-item">
<div class="item-label">
<PaperClipOutlined />
活动附件
</div>
<div class="item-value">
<div v-for="att in contest.attachments" :key="att.id" class="contest-attachment-link">
<a :href="att.fileUrl" target="_blank" rel="noopener noreferrer">
<PaperClipOutlined />
{{ att.fileName }}
</a>
</div>
</div>
</div>
</div>
</div>
@ -318,11 +283,7 @@
<div class="item-value">
<template v-if="contest.organizers">
<template v-if="Array.isArray(contest.organizers)">
<div
v-for="org in contest.organizers"
:key="org"
class="org-name"
>
<div v-for="org in contest.organizers" :key="org" class="org-name">
{{ org }}
</div>
</template>
@ -342,11 +303,7 @@
<div class="item-value">
<template v-if="contest.coOrganizers">
<template v-if="Array.isArray(contest.coOrganizers)">
<div
v-for="org in contest.coOrganizers"
:key="org"
class="org-name"
>
<div v-for="org in contest.coOrganizers" :key="org" class="org-name">
{{ org }}
</div>
</template>
@ -366,11 +323,7 @@
<div class="item-value">
<template v-if="contest.sponsors">
<template v-if="Array.isArray(contest.sponsors)">
<div
v-for="sp in contest.sponsors"
:key="sp"
class="org-name"
>
<div v-for="sp in contest.sponsors" :key="sp" class="org-name">
{{ sp }}
</div>
</template>
@ -381,14 +334,12 @@
<span v-else class="empty-text">暂无</span>
</div>
</div>
</div>
</div>
<!-- 联系方式 -->
<div
v-if="contest.contactName || contest.contactPhone"
class="sidebar-card"
>
<div v-if="contest.contactName || contest.contactPhone" class="sidebar-card">
<div class="sidebar-header">
<PhoneOutlined />
<span>联系方式</span>
@ -415,11 +366,7 @@
</div>
</div>
<a-empty
v-else-if="!loading"
description="活动不存在"
style="padding: 100px 0"
/>
<a-empty v-else-if="!loading" description="活动不存在" style="padding: 100px 0" />
</a-spin>
</div>
</template>
@ -445,6 +392,7 @@ import {
UserOutlined,
GlobalOutlined,
InfoCircleOutlined,
PaperClipOutlined,
} from "@ant-design/icons-vue"
import dayjs from "dayjs"
import { useAuthStore } from "@/stores/auth"
@ -731,11 +679,9 @@ $primary-dark: #0958d9;
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.6) 100%
);
background: linear-gradient(180deg,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.6) 100%);
}
.hero-nav {
@ -800,12 +746,15 @@ $primary-dark: #0958d9;
.tag-registering {
background: linear-gradient(135deg, #52c41a, #73d13d);
}
.tag-submitting {
background: linear-gradient(135deg, #1890ff, #40a9ff);
}
.tag-reviewing {
background: linear-gradient(135deg, #faad14, #ffc53d);
}
.tag-finished {
background: linear-gradient(135deg, #8c8c8c, #bfbfbf);
}
@ -1018,6 +967,7 @@ $primary-dark: #0958d9;
:deep(p) {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
@ -1033,6 +983,7 @@ $primary-dark: #0958d9;
font-weight: 600;
margin-top: 24px;
margin-bottom: 12px;
&:first-child {
margin-top: 0;
}
@ -1083,6 +1034,7 @@ $primary-dark: #0958d9;
:deep(a) {
color: $primary;
text-decoration: none;
&:hover {
text-decoration: underline;
}
@ -1123,6 +1075,7 @@ $primary-dark: #0958d9;
background: #fff2f0;
color: #ff4d4f;
}
&.system {
background: #e6f7ff;
color: #1890ff;
@ -1137,6 +1090,28 @@ $primary-dark: #0958d9;
margin-bottom: 10px;
}
.notice-attachments-block {
margin-bottom: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
.notice-attachments-title {
font-size: 12px;
font-weight: 600;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 6px;
}
.notice-att-link a {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: $primary;
margin-bottom: 4px;
}
}
.notice-time {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
@ -1164,10 +1139,12 @@ $primary-dark: #0958d9;
background: linear-gradient(135deg, #ffd700, #ffb800);
color: #fff;
}
&.rank-2 {
background: linear-gradient(135deg, #c0c0c0, #a0a0a0);
color: #fff;
}
&.rank-3 {
background: linear-gradient(135deg, #cd7f32, #b8860b);
color: #fff;
@ -1185,10 +1162,12 @@ $primary-dark: #0958d9;
background: #fffbe6;
color: #d48806;
}
&.award-silver {
background: #f5f5f5;
color: #595959;
}
&.award-bronze {
background: #fff7e6;
color: #d46b08;
@ -1212,11 +1191,9 @@ $primary-dark: #0958d9;
align-items: center;
gap: 10px;
padding: 16px 20px;
background: linear-gradient(
135deg,
rgba($primary, 0.05),
rgba($primary-dark, 0.08)
);
background: linear-gradient(135deg,
rgba($primary, 0.05),
rgba($primary-dark, 0.08));
border-bottom: 1px solid #f0f0f0;
font-size: 15px;
font-weight: 600;
@ -1269,6 +1246,15 @@ $primary-dark: #0958d9;
.empty-text {
color: rgba(0, 0, 0, 0.25);
}
.contest-attachment-link a {
display: inline-flex;
align-items: center;
gap: 6px;
color: $primary;
font-size: 13px;
padding: 4px 0;
}
}
}
}
@ -1299,10 +1285,12 @@ $primary-dark: #0958d9;
.hero-title {
font-size: 24px;
}
.hero-meta {
flex-direction: column;
gap: 12px;
}
.hero-actions {
flex-wrap: wrap;
}

View File

@ -147,6 +147,14 @@
<div v-for="notice in activity.notices" :key="notice.id" class="notice-item">
<h4>{{ notice.title }}</h4>
<div class="notice-content" v-html="sanitizeNoticeContent(notice.content)"></div>
<div v-if="notice.attachments?.length" class="notice-attachments">
<div class="notice-attachments-title">公告附件</div>
<div v-for="att in notice.attachments" :key="att.id" class="att-item">
<a :href="att.fileUrl" target="_blank" rel="noopener noreferrer">
<paper-clip-outlined /> {{ att.fileName }}
</a>
</div>
</div>
<span class="notice-time">{{ formatNoticeTime(notice) }}</span>
</div>
</div>
@ -262,7 +270,7 @@
</div>
<!-- 报名弹窗 -->
<a-modal v-model:open="showRegisterModal" title="活动报名" :footer="null" :width="420">
<a-modal v-model:open="showRegisterModal" title="活动报名" :footer="null" :width="420" centered>
<div class="register-modal">
<template v-if="isChildUser">
<p class="modal-desc">将使用当前账号报名确认参加本活动吗</p>
@ -1201,6 +1209,28 @@ $primary: #6366f1;
line-height: 1.6;
}
.notice-attachments {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e5e7eb;
.notice-attachments-title {
font-size: 12px;
font-weight: 600;
color: #6b7280;
margin-bottom: 6px;
}
.att-item a {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: $primary;
padding: 4px 0;
}
}
.notice-time {
font-size: 12px;
color: #9ca3af;

View File

@ -3,6 +3,7 @@
:open="open"
title="从作品库选择作品"
:width="600"
centered
@cancel="$emit('update:open', false)"
@ok="handleConfirm"
:ok-button-props="{ disabled: !selectedWork }"

View File

@ -68,6 +68,7 @@
title="创建子女账号"
:footer="null"
:width="440"
centered
>
<a-form :model="createForm" layout="vertical" @finish="handleCreate" class="child-form">
<a-form-item label="用户名" name="username" :rules="[{ required: true, message: '请输入用户名' }, { min: 4, message: '至少4个字符' }]">
@ -109,6 +110,7 @@
title="编辑子女账号"
:footer="null"
:width="440"
centered
>
<a-form :model="editForm" layout="vertical" @finish="handleEdit" class="child-form">
<a-form-item label="昵称" name="nickname">

View File

@ -69,7 +69,7 @@
</div>
<!-- 编辑个人信息弹窗 -->
<a-modal v-model:open="showEditModal" title="编辑个人信息" :footer="null" :width="400">
<a-modal v-model:open="showEditModal" title="编辑个人信息" :footer="null" :width="400" centered>
<a-form layout="vertical" @finish="handleSaveProfile" style="margin-top: 16px;">
<a-form-item label="昵称" :rules="[{ required: true, message: '请输入昵称' }]">
<a-input v-model:value="editForm.nickname" placeholder="你的昵称" :maxlength="20" />

View File

@ -158,7 +158,7 @@
</a-spin>
<!-- 二次确认弹窗 -->
<a-modal v-model:open="confirmVisible" :title="confirmTitle" :ok-text="confirmOkText" cancel-text="取消"
<a-modal v-model:open="confirmVisible" :title="confirmTitle" :ok-text="confirmOkText" cancel-text="取消" centered
:confirm-loading="actionLoading" @ok="handleConfirmOk" @cancel="handleConfirmCancel">
<p>{{ confirmContent }}</p>
</a-modal>