feat: 公告附件持久化与 OSS 支持 svg、编辑时加载活动列表
Made-with: Cursor
This commit is contained in:
parent
079aed72e8
commit
b8019ac4ee
@ -13,6 +13,8 @@ import com.lesingle.modules.biz.contest.dto.CreateNoticeDto;
|
|||||||
import com.lesingle.modules.biz.contest.entity.BizContestNotice;
|
import com.lesingle.modules.biz.contest.entity.BizContestNotice;
|
||||||
import com.lesingle.modules.biz.contest.service.IContestNoticeService;
|
import com.lesingle.modules.biz.contest.service.IContestNoticeService;
|
||||||
import com.lesingle.security.annotation.RequirePermission;
|
import com.lesingle.security.annotation.RequirePermission;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@ -33,6 +35,7 @@ import java.util.Map;
|
|||||||
public class ContestNoticeController {
|
public class ContestNoticeController {
|
||||||
|
|
||||||
private final IContestNoticeService noticeService;
|
private final IContestNoticeService noticeService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析日期时间字符串,兼容 ISO 格式(带毫秒和 Z 时区标记)
|
* 解析日期时间字符串,兼容 ISO 格式(带毫秒和 Z 时区标记)
|
||||||
@ -92,6 +95,7 @@ public class ContestNoticeController {
|
|||||||
.eq(BizContestNotice::getTenantId, tenantId)
|
.eq(BizContestNotice::getTenantId, tenantId)
|
||||||
.orderByDesc(BizContestNotice::getCreateTime));
|
.orderByDesc(BizContestNotice::getCreateTime));
|
||||||
noticeService.fillContestInfo(list);
|
noticeService.fillContestInfo(list);
|
||||||
|
noticeService.fillNoticeAttachments(list);
|
||||||
return Result.success(list);
|
return Result.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +122,7 @@ public class ContestNoticeController {
|
|||||||
wrapper.orderByDesc(BizContestNotice::getCreateTime);
|
wrapper.orderByDesc(BizContestNotice::getCreateTime);
|
||||||
Page<BizContestNotice> result = noticeService.page(new Page<>(page, pageSize), wrapper);
|
Page<BizContestNotice> result = noticeService.page(new Page<>(page, pageSize), wrapper);
|
||||||
noticeService.fillContestInfo(result.getRecords());
|
noticeService.fillContestInfo(result.getRecords());
|
||||||
|
noticeService.fillNoticeAttachments(result.getRecords());
|
||||||
return Result.success(PageResult.from(result));
|
return Result.success(PageResult.from(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +133,7 @@ public class ContestNoticeController {
|
|||||||
BizContestNotice notice = noticeService.getById(id);
|
BizContestNotice notice = noticeService.getById(id);
|
||||||
if (notice != null) {
|
if (notice != null) {
|
||||||
noticeService.fillContestInfo(Collections.singletonList(notice));
|
noticeService.fillContestInfo(Collections.singletonList(notice));
|
||||||
|
noticeService.fillNoticeAttachments(Collections.singletonList(notice));
|
||||||
}
|
}
|
||||||
return Result.success(notice);
|
return Result.success(notice);
|
||||||
}
|
}
|
||||||
@ -149,40 +155,40 @@ public class ContestNoticeController {
|
|||||||
uw.eq(BizContestNotice::getId, id);
|
uw.eq(BizContestNotice::getId, id);
|
||||||
uw.eq(BizContestNotice::getTenantId, tenantId);
|
uw.eq(BizContestNotice::getTenantId, tenantId);
|
||||||
|
|
||||||
boolean hasUpdate = false;
|
boolean hasFieldUpdate = false;
|
||||||
if (body.containsKey("title")) {
|
if (body.containsKey("title")) {
|
||||||
Object v = body.get("title");
|
Object v = body.get("title");
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
uw.set(BizContestNotice::getTitle, String.valueOf(v));
|
uw.set(BizContestNotice::getTitle, String.valueOf(v));
|
||||||
hasUpdate = true;
|
hasFieldUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (body.containsKey("content")) {
|
if (body.containsKey("content")) {
|
||||||
Object v = body.get("content");
|
Object v = body.get("content");
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
uw.set(BizContestNotice::getContent, String.valueOf(v));
|
uw.set(BizContestNotice::getContent, String.valueOf(v));
|
||||||
hasUpdate = true;
|
hasFieldUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (body.containsKey("noticeType")) {
|
if (body.containsKey("noticeType")) {
|
||||||
Object v = body.get("noticeType");
|
Object v = body.get("noticeType");
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
uw.set(BizContestNotice::getNoticeType, String.valueOf(v));
|
uw.set(BizContestNotice::getNoticeType, String.valueOf(v));
|
||||||
hasUpdate = true;
|
hasFieldUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (body.containsKey("priority")) {
|
if (body.containsKey("priority")) {
|
||||||
Object v = body.get("priority");
|
Object v = body.get("priority");
|
||||||
if (v instanceof Number) {
|
if (v instanceof Number) {
|
||||||
uw.set(BizContestNotice::getPriority, ((Number) v).intValue());
|
uw.set(BizContestNotice::getPriority, ((Number) v).intValue());
|
||||||
hasUpdate = true;
|
hasFieldUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (body.containsKey("contestId")) {
|
if (body.containsKey("contestId")) {
|
||||||
Object v = body.get("contestId");
|
Object v = body.get("contestId");
|
||||||
if (v instanceof Number) {
|
if (v instanceof Number) {
|
||||||
uw.set(BizContestNotice::getContestId, ((Number) v).longValue());
|
uw.set(BizContestNotice::getContestId, ((Number) v).longValue());
|
||||||
hasUpdate = true;
|
hasFieldUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 仅当请求体包含 publishTime 键时才改发布时间:null / 空串 = 取消发布(必须写入 SQL NULL)
|
// 仅当请求体包含 publishTime 键时才改发布时间:null / 空串 = 取消发布(必须写入 SQL NULL)
|
||||||
@ -190,20 +196,36 @@ public class ContestNoticeController {
|
|||||||
Object v = body.get("publishTime");
|
Object v = body.get("publishTime");
|
||||||
if (v == null || (v instanceof String && !StringUtils.hasText((String) v))) {
|
if (v == null || (v instanceof String && !StringUtils.hasText((String) v))) {
|
||||||
uw.set(BizContestNotice::getPublishTime, null);
|
uw.set(BizContestNotice::getPublishTime, null);
|
||||||
hasUpdate = true;
|
hasFieldUpdate = true;
|
||||||
} else if (v instanceof String && StringUtils.hasText((String) v)) {
|
} else if (v instanceof String && StringUtils.hasText((String) v)) {
|
||||||
LocalDateTime pt = parseDateTime((String) v);
|
LocalDateTime pt = parseDateTime((String) v);
|
||||||
if (pt != null) {
|
if (pt != null) {
|
||||||
uw.set(BizContestNotice::getPublishTime, pt);
|
uw.set(BizContestNotice::getPublishTime, pt);
|
||||||
hasUpdate = true;
|
hasFieldUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasUpdate) {
|
boolean attachmentSynced = false;
|
||||||
|
if (body.containsKey("attachments")) {
|
||||||
|
Object raw = body.get("attachments");
|
||||||
|
List<CreateNoticeDto.NoticeAttachmentItem> items;
|
||||||
|
if (raw == null) {
|
||||||
|
items = Collections.emptyList();
|
||||||
|
} else {
|
||||||
|
items = objectMapper.convertValue(raw, new TypeReference<List<CreateNoticeDto.NoticeAttachmentItem>>() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
noticeService.syncNoticeAttachments(id, tenantId, items);
|
||||||
|
attachmentSynced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFieldUpdate && !attachmentSynced) {
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
noticeService.getBaseMapper().update(null, uw);
|
if (hasFieldUpdate) {
|
||||||
|
noticeService.getBaseMapper().update(null, uw);
|
||||||
|
}
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "创建公告DTO")
|
@Schema(description = "创建公告DTO")
|
||||||
public class CreateNoticeDto {
|
public class CreateNoticeDto {
|
||||||
@ -29,4 +31,30 @@ public class CreateNoticeDto {
|
|||||||
|
|
||||||
@Schema(description = "发布时间")
|
@Schema(description = "发布时间")
|
||||||
private String publishTime;
|
private String publishTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告附件全量列表;与 PATCH 对齐。为 null 或省略表示创建时不写附件;传空数组表示无附件。
|
||||||
|
*/
|
||||||
|
@Schema(description = "公告附件列表:id 为空表示新增,已有 id 表示保留/更新;全量覆盖")
|
||||||
|
private List<NoticeAttachmentItem> attachments;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "公告附件项")
|
||||||
|
public static class NoticeAttachmentItem {
|
||||||
|
|
||||||
|
@Schema(description = "附件主键,新建不传")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "文件名")
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@Schema(description = "文件访问 URL")
|
||||||
|
private String fileUrl;
|
||||||
|
|
||||||
|
private String format;
|
||||||
|
|
||||||
|
private String fileType;
|
||||||
|
|
||||||
|
private String size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 活动公告实体
|
* 活动公告实体
|
||||||
@ -47,4 +48,9 @@ public class BizContestNotice extends BaseEntity {
|
|||||||
@Schema(description = "关联活动")
|
@Schema(description = "关联活动")
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private BizContest contest;
|
private BizContest contest;
|
||||||
|
|
||||||
|
/** 公告附件(仅查询接口填充,不落库) */
|
||||||
|
@Schema(description = "公告附件列表")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private List<BizContestNoticeAttachment> attachments;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.lesingle.modules.biz.contest.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.lesingle.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动公告附件
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("t_biz_contest_notice_attachment")
|
||||||
|
@Schema(description = "活动公告附件实体")
|
||||||
|
public class BizContestNoticeAttachment extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "公告ID")
|
||||||
|
@TableField("notice_id")
|
||||||
|
private Long noticeId;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
|
@TableField("tenant_id")
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "文件名称")
|
||||||
|
@TableField("file_name")
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@Schema(description = "文件URL")
|
||||||
|
@TableField("file_url")
|
||||||
|
private String fileUrl;
|
||||||
|
|
||||||
|
@Schema(description = "文件格式")
|
||||||
|
private String format;
|
||||||
|
|
||||||
|
@Schema(description = "文件类型")
|
||||||
|
@TableField("file_type")
|
||||||
|
private String fileType;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小")
|
||||||
|
private String size;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.lesingle.modules.biz.contest.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.lesingle.modules.biz.contest.entity.BizContestNoticeAttachment;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ContestNoticeAttachmentMapper extends BaseMapper<BizContestNoticeAttachment> {
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package com.lesingle.modules.biz.contest.service;
|
package com.lesingle.modules.biz.contest.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.lesingle.modules.biz.contest.dto.CreateNoticeDto;
|
||||||
import com.lesingle.modules.biz.contest.entity.BizContestNotice;
|
import com.lesingle.modules.biz.contest.entity.BizContestNotice;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -11,4 +12,14 @@ public interface IContestNoticeService extends IService<BizContestNotice> {
|
|||||||
* 批量填充关联活动名称(仅设置 id、contestName,供前端展示)
|
* 批量填充关联活动名称(仅设置 id、contestName,供前端展示)
|
||||||
*/
|
*/
|
||||||
void fillContestInfo(List<BizContestNotice> notices);
|
void fillContestInfo(List<BizContestNotice> notices);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充公告附件(当前租户)
|
||||||
|
*/
|
||||||
|
void fillNoticeAttachments(List<BizContestNotice> notices);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全量同步公告附件:列表中有 id 的保留并更新;无 id 的新增;库中已有但未出现在列表中的删除。
|
||||||
|
*/
|
||||||
|
void syncNoticeAttachments(Long noticeId, Long tenantId, List<CreateNoticeDto.NoticeAttachmentItem> items);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,25 @@
|
|||||||
package com.lesingle.modules.biz.contest.service.impl;
|
package com.lesingle.modules.biz.contest.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.lesingle.common.enums.ErrorCode;
|
||||||
|
import com.lesingle.common.exception.BusinessException;
|
||||||
|
import com.lesingle.common.util.SecurityUtil;
|
||||||
|
import com.lesingle.modules.biz.contest.dto.CreateNoticeDto;
|
||||||
import com.lesingle.modules.biz.contest.entity.BizContest;
|
import com.lesingle.modules.biz.contest.entity.BizContest;
|
||||||
import com.lesingle.modules.biz.contest.entity.BizContestNotice;
|
import com.lesingle.modules.biz.contest.entity.BizContestNotice;
|
||||||
|
import com.lesingle.modules.biz.contest.entity.BizContestNoticeAttachment;
|
||||||
|
import com.lesingle.modules.biz.contest.mapper.ContestNoticeAttachmentMapper;
|
||||||
import com.lesingle.modules.biz.contest.mapper.ContestNoticeMapper;
|
import com.lesingle.modules.biz.contest.mapper.ContestNoticeMapper;
|
||||||
import com.lesingle.modules.biz.contest.service.IContestNoticeService;
|
import com.lesingle.modules.biz.contest.service.IContestNoticeService;
|
||||||
import com.lesingle.modules.biz.contest.service.IContestService;
|
import com.lesingle.modules.biz.contest.service.IContestService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -21,6 +32,7 @@ import java.util.stream.Collectors;
|
|||||||
public class ContestNoticeServiceImpl extends ServiceImpl<ContestNoticeMapper, BizContestNotice> implements IContestNoticeService {
|
public class ContestNoticeServiceImpl extends ServiceImpl<ContestNoticeMapper, BizContestNotice> implements IContestNoticeService {
|
||||||
|
|
||||||
private final IContestService contestService;
|
private final IContestService contestService;
|
||||||
|
private final ContestNoticeAttachmentMapper noticeAttachmentMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fillContestInfo(List<BizContestNotice> notices) {
|
public void fillContestInfo(List<BizContestNotice> notices) {
|
||||||
@ -51,4 +63,117 @@ public class ContestNoticeServiceImpl extends ServiceImpl<ContestNoticeMapper, B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fillNoticeAttachments(List<BizContestNotice> notices) {
|
||||||
|
if (notices == null || notices.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<Long> ids = notices.stream()
|
||||||
|
.map(BizContestNotice::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||||
|
LambdaQueryWrapper<BizContestNoticeAttachment> qw = new LambdaQueryWrapper<>();
|
||||||
|
qw.in(BizContestNoticeAttachment::getNoticeId, ids);
|
||||||
|
qw.eq(BizContestNoticeAttachment::getTenantId, tenantId);
|
||||||
|
qw.orderByAsc(BizContestNoticeAttachment::getCreateTime);
|
||||||
|
List<BizContestNoticeAttachment> all = noticeAttachmentMapper.selectList(qw);
|
||||||
|
Map<Long, List<BizContestNoticeAttachment>> byNotice = all.stream()
|
||||||
|
.collect(Collectors.groupingBy(BizContestNoticeAttachment::getNoticeId));
|
||||||
|
for (BizContestNotice notice : notices) {
|
||||||
|
notice.setAttachments(byNotice.getOrDefault(notice.getId(), Collections.emptyList()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void syncNoticeAttachments(Long noticeId, Long tenantId, List<CreateNoticeDto.NoticeAttachmentItem> items) {
|
||||||
|
if (items == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BizContestNotice n = getOne(
|
||||||
|
new LambdaQueryWrapper<BizContestNotice>()
|
||||||
|
.eq(BizContestNotice::getId, noticeId)
|
||||||
|
.eq(BizContestNotice::getTenantId, tenantId));
|
||||||
|
if (n == null) {
|
||||||
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "公告不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<BizContestNoticeAttachment> qw = new LambdaQueryWrapper<>();
|
||||||
|
qw.eq(BizContestNoticeAttachment::getNoticeId, noticeId);
|
||||||
|
qw.eq(BizContestNoticeAttachment::getTenantId, tenantId);
|
||||||
|
List<BizContestNoticeAttachment> existing = noticeAttachmentMapper.selectList(qw);
|
||||||
|
Set<Long> keepIds = items.stream()
|
||||||
|
.map(CreateNoticeDto.NoticeAttachmentItem::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
for (BizContestNoticeAttachment row : existing) {
|
||||||
|
if (!keepIds.contains(row.getId())) {
|
||||||
|
noticeAttachmentMapper.deleteById(row.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (CreateNoticeDto.NoticeAttachmentItem item : items) {
|
||||||
|
if (!StringUtils.hasText(item.getFileUrl())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String fileName = StringUtils.hasText(item.getFileName()) ? item.getFileName().trim() : "附件";
|
||||||
|
|
||||||
|
if (item.getId() == null) {
|
||||||
|
BizContestNoticeAttachment na = new BizContestNoticeAttachment();
|
||||||
|
na.setNoticeId(noticeId);
|
||||||
|
na.setTenantId(tenantId);
|
||||||
|
na.setFileName(fileName);
|
||||||
|
na.setFileUrl(item.getFileUrl().trim());
|
||||||
|
na.setFormat(item.getFormat());
|
||||||
|
na.setFileType(item.getFileType());
|
||||||
|
na.setSize(item.getSize());
|
||||||
|
na.setValidState(1);
|
||||||
|
noticeAttachmentMapper.insert(na);
|
||||||
|
} else {
|
||||||
|
BizContestNoticeAttachment a = noticeAttachmentMapper.selectById(item.getId());
|
||||||
|
if (a == null || !noticeId.equals(a.getNoticeId()) || !tenantId.equals(a.getTenantId())) {
|
||||||
|
throw BusinessException.of(ErrorCode.BAD_REQUEST, "附件不存在或不属于该公告:" + item.getId());
|
||||||
|
}
|
||||||
|
a.setFileName(fileName);
|
||||||
|
a.setFileUrl(item.getFileUrl().trim());
|
||||||
|
if (item.getFormat() != null) {
|
||||||
|
a.setFormat(item.getFormat());
|
||||||
|
}
|
||||||
|
if (item.getFileType() != null) {
|
||||||
|
a.setFileType(item.getFileType());
|
||||||
|
}
|
||||||
|
if (item.getSize() != null) {
|
||||||
|
a.setSize(item.getSize());
|
||||||
|
}
|
||||||
|
noticeAttachmentMapper.updateById(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean removeById(Serializable id) {
|
||||||
|
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||||
|
BizContestNotice n = getOne(
|
||||||
|
new LambdaQueryWrapper<BizContestNotice>()
|
||||||
|
.eq(BizContestNotice::getId, id)
|
||||||
|
.eq(BizContestNotice::getTenantId, tenantId));
|
||||||
|
if (n == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<BizContestNoticeAttachment> aw = new LambdaQueryWrapper<>();
|
||||||
|
aw.eq(BizContestNoticeAttachment::getNoticeId, id);
|
||||||
|
aw.eq(BizContestNoticeAttachment::getTenantId, tenantId);
|
||||||
|
List<BizContestNoticeAttachment> rows = noticeAttachmentMapper.selectList(aw);
|
||||||
|
for (BizContestNoticeAttachment row : rows) {
|
||||||
|
noticeAttachmentMapper.deleteById(row.getId());
|
||||||
|
}
|
||||||
|
return super.removeById(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ public class OssConfig {
|
|||||||
|
|
||||||
/** 允许的文件扩展名 */
|
/** 允许的文件扩展名 */
|
||||||
private String[] allowedExtensions = {
|
private String[] allowedExtensions = {
|
||||||
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp",
|
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg",
|
||||||
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
||||||
".mp4", ".mp3", ".wav", ".avi",
|
".mp4", ".mp3", ".wav", ".avi",
|
||||||
".zip", ".rar",
|
".zip", ".rar",
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
-- 公告附件表(与活动附件独立,按 notice_id 归属)
|
||||||
|
CREATE TABLE t_biz_contest_notice_attachment (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
notice_id BIGINT NOT NULL COMMENT '公告ID',
|
||||||
|
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||||
|
file_name VARCHAR(512) NOT NULL COMMENT '文件名称',
|
||||||
|
file_url VARCHAR(2048) NOT NULL COMMENT '文件URL',
|
||||||
|
format VARCHAR(32) DEFAULT NULL COMMENT '扩展名',
|
||||||
|
file_type VARCHAR(128) DEFAULT NULL COMMENT 'MIME 类型',
|
||||||
|
size VARCHAR(64) DEFAULT NULL COMMENT '文件大小',
|
||||||
|
create_by VARCHAR(64) DEFAULT NULL COMMENT '创建人账号',
|
||||||
|
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人账号',
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
creator INT DEFAULT NULL COMMENT '创建人ID',
|
||||||
|
modifier INT DEFAULT NULL COMMENT '修改人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
modify_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
valid_state TINYINT NOT NULL DEFAULT 1 COMMENT '有效状态:1-有效,2-失效',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_notice_id (notice_id),
|
||||||
|
INDEX idx_tenant_notice (tenant_id, notice_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动公告附件表';
|
||||||
@ -542,6 +542,18 @@ export interface CreateScoreForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 公告相关类型 ====================
|
// ==================== 公告相关类型 ====================
|
||||||
|
/** 公告附件(与后端 BizContestNoticeAttachment 对齐) */
|
||||||
|
export interface ContestNoticeAttachment {
|
||||||
|
id: number;
|
||||||
|
noticeId?: number;
|
||||||
|
fileName: string;
|
||||||
|
fileUrl: string;
|
||||||
|
format?: string;
|
||||||
|
fileType?: string;
|
||||||
|
size?: string;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContestNotice {
|
export interface ContestNotice {
|
||||||
id: number;
|
id: number;
|
||||||
contestId: number;
|
contestId: number;
|
||||||
@ -556,6 +568,8 @@ export interface ContestNotice {
|
|||||||
modifyTime?: string;
|
modifyTime?: string;
|
||||||
validState?: number;
|
validState?: number;
|
||||||
contest?: Contest;
|
contest?: Contest;
|
||||||
|
/** 公告附件(详情/列表接口填充) */
|
||||||
|
attachments?: ContestNoticeAttachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateNoticeForm {
|
export interface CreateNoticeForm {
|
||||||
@ -564,6 +578,8 @@ export interface CreateNoticeForm {
|
|||||||
content: string;
|
content: string;
|
||||||
noticeType?: "system" | "manual" | "urgent";
|
noticeType?: "system" | "manual" | "urgent";
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
/** 与后端 CreateNoticeDto.attachments 一致;创建时随 POST 全量提交 */
|
||||||
|
attachments?: ContestAttachmentInput[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 评委相关类型 ====================
|
// ==================== 评委相关类型 ====================
|
||||||
|
|||||||
@ -125,8 +125,8 @@
|
|||||||
<a-textarea v-model:value="formData.content" placeholder="请输入正文内容" :rows="8" />
|
<a-textarea v-model:value="formData.content" placeholder="请输入正文内容" :rows="8" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="活动附件">
|
<a-form-item label="活动附件">
|
||||||
<a-upload v-model:file-list="attachmentFileList" :before-upload="beforeUpload" :custom-request="handleUpload"
|
<a-upload v-model:file-list="attachmentFileList" :before-upload="beforeFileUpload"
|
||||||
:on-remove="handleRemoveAttachment">
|
:custom-request="handleAttachmentUpload" :max-count="10" @change="handleAttachmentListChange">
|
||||||
<a-button>
|
<a-button>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<UploadOutlined />
|
<UploadOutlined />
|
||||||
@ -134,26 +134,8 @@
|
|||||||
上传附件
|
上传附件
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
<div v-if="attachmentList.length > 0" style="margin-top: 16px">
|
<div style="margin-top: 8px; color: rgba(0, 0, 0, 0.45); font-size: 12px">
|
||||||
<div v-for="attachment in attachmentList" :key="attachment.id" style="
|
最多 10 个文件,每个不超过 30M(OSS 直传)
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
">
|
|
||||||
<span>{{ attachment.fileName }}</span>
|
|
||||||
<a-space>
|
|
||||||
<a-button type="link" size="small" @click="handleDownload(attachment)">
|
|
||||||
下载
|
|
||||||
</a-button>
|
|
||||||
<a-button type="link" danger size="small" @click="handleDeleteAttachment(attachment.id)">
|
|
||||||
删除
|
|
||||||
</a-button>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
@ -171,7 +153,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, reactive } from "vue"
|
import { ref, onMounted, reactive, nextTick } from "vue"
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
import { message, Modal } from "ant-design-vue"
|
import { message, Modal } from "ant-design-vue"
|
||||||
import { PlusOutlined, UploadOutlined, SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
import { PlusOutlined, UploadOutlined, SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||||
@ -180,12 +162,13 @@ import type { FormInstance, UploadFile } from "ant-design-vue"
|
|||||||
import {
|
import {
|
||||||
contestsApi,
|
contestsApi,
|
||||||
noticesApi,
|
noticesApi,
|
||||||
attachmentsApi,
|
|
||||||
type Contest,
|
type Contest,
|
||||||
|
type ContestAttachmentInput,
|
||||||
type ContestNotice,
|
type ContestNotice,
|
||||||
|
type ContestNoticeAttachment,
|
||||||
type CreateNoticeForm,
|
type CreateNoticeForm,
|
||||||
type ContestAttachment,
|
|
||||||
} from "@/api/contests"
|
} from "@/api/contests"
|
||||||
|
import { uploadFile } from "@/api/upload"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const tenantCode = route.params.tenantCode as string
|
const tenantCode = route.params.tenantCode as string
|
||||||
@ -225,9 +208,8 @@ const formData = reactive<CreateNoticeForm>({
|
|||||||
const contestsList = ref<Contest[]>([])
|
const contestsList = ref<Contest[]>([])
|
||||||
const contestsLoading = ref(false)
|
const contestsLoading = ref(false)
|
||||||
|
|
||||||
// 附件
|
// 附件(与 contests/Create.vue 一致:OSS 直传 contest/attachment)
|
||||||
const attachmentFileList = ref<UploadFile[]>([])
|
const attachmentFileList = ref<UploadFile[]>([])
|
||||||
const attachmentList = ref<ContestAttachment[]>([])
|
|
||||||
|
|
||||||
const formRules = {
|
const formRules = {
|
||||||
title: [{ required: true, message: "请输入标题名称", trigger: "blur" }],
|
title: [{ required: true, message: "请输入标题名称", trigger: "blur" }],
|
||||||
@ -291,9 +273,12 @@ const formatDateTime = (dateStr?: string) => {
|
|||||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤活动选项
|
// 过滤活动选项(Vue3 + Select 的 option.children 可能为 VNode,不可 toLowerCase,按 value 查 contestsList)
|
||||||
const filterContestOption = (input: string, option: any) => {
|
const filterContestOption = (input: string, option: any) => {
|
||||||
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
const id = option?.value ?? option?.option?.value
|
||||||
|
const contest = contestsList.value.find((c) => c.id === id)
|
||||||
|
const name = contest?.contestName ?? ""
|
||||||
|
return name.toLowerCase().includes((input ?? "").toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载活动列表
|
// 加载活动列表
|
||||||
@ -351,23 +336,6 @@ const fetchNotices = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载附件列表
|
|
||||||
const fetchAttachments = async (noticeId: number) => {
|
|
||||||
try {
|
|
||||||
// TODO: 需要后端API支持获取公告附件
|
|
||||||
// 临时方案:使用活动附件API
|
|
||||||
const notice = dataSource.value.find((n) => n.id === noticeId)
|
|
||||||
if (notice) {
|
|
||||||
const attachments = await attachmentsApi.getList(notice.contestId)
|
|
||||||
attachmentList.value = attachments.filter(
|
|
||||||
(a) => a.contestId === notice.contestId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取附件列表失败", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表格变化
|
// 表格变化
|
||||||
const handleTableChange = (pag: any) => {
|
const handleTableChange = (pag: any) => {
|
||||||
pagination.current = pag.current
|
pagination.current = pag.current
|
||||||
@ -403,7 +371,6 @@ const handleAdd = async () => {
|
|||||||
formData.contestId = undefined;
|
formData.contestId = undefined;
|
||||||
formData.title = ""
|
formData.title = ""
|
||||||
formData.content = ""
|
formData.content = ""
|
||||||
attachmentList.value = []
|
|
||||||
attachmentFileList.value = []
|
attachmentFileList.value = []
|
||||||
|
|
||||||
// 打开抽屉时加载活动列表
|
// 打开抽屉时加载活动列表
|
||||||
@ -412,15 +379,67 @@ const handleAdd = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
|
function attachmentToUploadFile(
|
||||||
|
att: ContestNoticeAttachment | Record<string, unknown>,
|
||||||
|
index: number,
|
||||||
|
): UploadFile {
|
||||||
|
const raw = att as Record<string, unknown>
|
||||||
|
const id = Number((raw.id as number | string | undefined) ?? 0)
|
||||||
|
const fileName = String(raw.fileName ?? raw.file_name ?? "附件")
|
||||||
|
const fileUrl = String(raw.fileUrl ?? raw.file_url ?? "")
|
||||||
|
return {
|
||||||
|
uid: id > 0 ? `exist-${id}` : `tmp-${index}-${fileName}`,
|
||||||
|
name: fileName,
|
||||||
|
status: "done",
|
||||||
|
url: fileUrl,
|
||||||
|
size: raw.size != null ? Number(raw.size) : undefined,
|
||||||
|
response: id > 0 ? { id } : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与后端 CreateNoticeDto.attachments / PATCH 全量同步对齐 */
|
||||||
|
function buildAttachmentsPayload(): ContestAttachmentInput[] {
|
||||||
|
const out: ContestAttachmentInput[] = []
|
||||||
|
for (const f of attachmentFileList.value) {
|
||||||
|
if (f.status !== "done") continue
|
||||||
|
const fileUrl = getAttachmentFileUrl(f)
|
||||||
|
const fileName = getAttachmentFileName(f)
|
||||||
|
if (!fileUrl) continue
|
||||||
|
const rid = (f.response as { id?: number } | undefined)?.id
|
||||||
|
const item: ContestAttachmentInput = {
|
||||||
|
fileName,
|
||||||
|
fileUrl,
|
||||||
|
format: fileName.includes(".")
|
||||||
|
? fileName.split(".").pop()?.toLowerCase()
|
||||||
|
: undefined,
|
||||||
|
size: f.size != null ? String(f.size) : undefined,
|
||||||
|
}
|
||||||
|
if (typeof rid === "number" && rid > 0) {
|
||||||
|
item.id = rid
|
||||||
|
}
|
||||||
|
out.push(item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
const handleEdit = async (record: ContestNotice) => {
|
const handleEdit = async (record: ContestNotice) => {
|
||||||
isEditing.value = true
|
isEditing.value = true
|
||||||
editingId.value = record.id
|
editingId.value = record.id
|
||||||
formData.contestId = record.contestId
|
formData.contestId = record.contestId
|
||||||
formData.title = record.title
|
formData.title = record.title
|
||||||
formData.content = record.content
|
formData.content = record.content
|
||||||
attachmentList.value = []
|
|
||||||
attachmentFileList.value = []
|
attachmentFileList.value = []
|
||||||
await fetchAttachments(record.id)
|
// 与新建一致:打开抽屉前拉取活动下拉数据,否则 contestsList 为空
|
||||||
|
await fetchContests()
|
||||||
|
try {
|
||||||
|
const detail = await noticesApi.getDetail(record.id)
|
||||||
|
const list = detail.attachments ?? []
|
||||||
|
attachmentFileList.value = list.map((a, i) =>
|
||||||
|
attachmentToUploadFile(a as ContestNoticeAttachment, i),
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
/* 详情失败时仍打开抽屉,附件为空 */
|
||||||
|
}
|
||||||
drawerVisible.value = true
|
drawerVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -462,55 +481,94 @@ const handleTogglePublish = (record: ContestNotice) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传前检查
|
function getAttachmentFileUrl(f: UploadFile): string {
|
||||||
const beforeUpload = (_file: File) => {
|
const r = f.response as { url?: string; data?: { url?: string } } | undefined
|
||||||
// 可以添加文件大小、类型等验证
|
return (
|
||||||
return true
|
String(f.url || "").trim() ||
|
||||||
}
|
String(f.thumbUrl || "").trim() ||
|
||||||
|
String(r?.url || "").trim() ||
|
||||||
// 上传文件
|
String(r?.data?.url || "").trim()
|
||||||
const handleUpload = async (options: any) => {
|
|
||||||
const { file, onSuccess, onError } = options
|
|
||||||
try {
|
|
||||||
// TODO: 调用实际上传接口
|
|
||||||
// 临时方案:先添加到附件列表
|
|
||||||
const attachment: ContestAttachment = {
|
|
||||||
id: Date.now(),
|
|
||||||
contestId: formData.contestId,
|
|
||||||
fileName: file.name,
|
|
||||||
fileUrl: URL.createObjectURL(file),
|
|
||||||
size: String(file.size),
|
|
||||||
}
|
|
||||||
attachmentList.value.push(attachment)
|
|
||||||
onSuccess()
|
|
||||||
message.success("上传成功")
|
|
||||||
} catch (error) {
|
|
||||||
onError(error)
|
|
||||||
message.error("上传失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除附件
|
|
||||||
const handleRemoveAttachment = (file: UploadFile) => {
|
|
||||||
// 从附件列表中移除
|
|
||||||
attachmentList.value = attachmentList.value.filter(
|
|
||||||
(a) => a.fileName !== file.name
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载附件
|
function getAttachmentFileName(f: UploadFile): string {
|
||||||
const handleDownload = (attachment: ContestAttachment) => {
|
const r = f.response as { filename?: string; originalname?: string } | undefined
|
||||||
window.open(attachment.fileUrl, "_blank")
|
return (
|
||||||
|
String(f.name || "").trim() ||
|
||||||
|
String(r?.filename || "").trim() ||
|
||||||
|
String(r?.originalname || "").trim() ||
|
||||||
|
(f.originFileObj && "name" in f.originFileObj
|
||||||
|
? String((f.originFileObj as File).name || "").trim()
|
||||||
|
: "") ||
|
||||||
|
"附件"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除附件
|
/** 从 response 补全 Upload 条目上的 url/name */
|
||||||
const handleDeleteAttachment = async (id: number) => {
|
function normalizeAttachmentFileList() {
|
||||||
|
attachmentFileList.value = attachmentFileList.value.map((item) => {
|
||||||
|
if (item.status !== "done") return item
|
||||||
|
const url = getAttachmentFileUrl(item)
|
||||||
|
const name = getAttachmentFileName(item)
|
||||||
|
const r = item.response as Record<string, unknown> | undefined
|
||||||
|
const mergedResp =
|
||||||
|
r && url
|
||||||
|
? { ...r, url: (r.url as string) || url }
|
||||||
|
: url
|
||||||
|
? { url, ...(typeof r === "object" && r ? r : {}) }
|
||||||
|
: r
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
url: item.url || url,
|
||||||
|
name: item.name || name,
|
||||||
|
response: mergedResp,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAttachmentListChange = () => {
|
||||||
|
nextTick(() => normalizeAttachmentFileList())
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeFileUpload = (file: File) => {
|
||||||
|
if (file.size / 1024 / 1024 >= 30) {
|
||||||
|
message.error("文件大小不能超过30M!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAttachmentUpload = async (options: any) => {
|
||||||
|
const { file, onSuccess, onError } = options
|
||||||
try {
|
try {
|
||||||
// TODO: 调用后端删除接口
|
const result = await uploadFile(file as File, "contest/attachment")
|
||||||
attachmentList.value = attachmentList.value.filter((a) => a.id !== id)
|
const url = result.url
|
||||||
message.success("删除成功")
|
if (!url) throw new Error("无法获取文件地址")
|
||||||
} catch (error: any) {
|
const mergedResponse = { ...result, url }
|
||||||
message.error(error?.response?.data?.message || "删除失败")
|
onSuccess(mergedResponse, file)
|
||||||
|
await nextTick()
|
||||||
|
const idx = attachmentFileList.value.findIndex(
|
||||||
|
(f) => f.uid === (file as UploadFile).uid,
|
||||||
|
)
|
||||||
|
if (idx !== -1) {
|
||||||
|
const prev = attachmentFileList.value[idx]
|
||||||
|
attachmentFileList.value[idx] = {
|
||||||
|
...prev,
|
||||||
|
name: prev.name || (file as UploadFile).name || result.filename,
|
||||||
|
url,
|
||||||
|
status: "done",
|
||||||
|
response: mergedResponse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizeAttachmentFileList()
|
||||||
|
message.success("附件上传成功")
|
||||||
|
} catch (e: any) {
|
||||||
|
const idx = attachmentFileList.value.findIndex(
|
||||||
|
(f) => f.uid === (file as UploadFile).uid,
|
||||||
|
)
|
||||||
|
if (idx !== -1) attachmentFileList.value[idx].status = "error"
|
||||||
|
onError(e)
|
||||||
|
message.error(e?.response?.data?.message || "附件上传失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -521,18 +579,21 @@ const handleSubmit = async () => {
|
|||||||
await formRef.value?.validate()
|
await formRef.value?.validate()
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
|
|
||||||
|
const attachments = buildAttachmentsPayload()
|
||||||
if (isEditing.value && editingId.value) {
|
if (isEditing.value && editingId.value) {
|
||||||
await noticesApi.update(editingId.value, {
|
await noticesApi.update(editingId.value, {
|
||||||
contestId: formData.contestId,
|
contestId: formData.contestId,
|
||||||
title: formData.title,
|
title: formData.title,
|
||||||
content: formData.content,
|
content: formData.content,
|
||||||
|
attachments,
|
||||||
})
|
})
|
||||||
message.success("更新成功")
|
message.success("更新成功")
|
||||||
} else {
|
} else {
|
||||||
await noticesApi.create({
|
await noticesApi.create({
|
||||||
contestId: formData.contestId,
|
contestId: formData.contestId!,
|
||||||
title: formData.title,
|
title: formData.title,
|
||||||
content: formData.content,
|
content: formData.content,
|
||||||
|
attachments,
|
||||||
})
|
})
|
||||||
message.success("创建成功")
|
message.success("创建成功")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,7 @@ public class OssConfig {
|
|||||||
private String[] allowedExtensions = new String[]{
|
private String[] allowedExtensions = new String[]{
|
||||||
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp",
|
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp",
|
||||||
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
||||||
".mp4", ".avi", ".mov", ".wmv",
|
".mp4", ".avi", ".mov", ".wmv", ".svg",
|
||||||
".mp3", ".wav",
|
".mp3", ".wav",
|
||||||
".txt"
|
".txt"
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user