feat: 活动创建/编辑附件与 PATCH 主体同步及表单对齐
- 后端 CreateContestDto 增加 attachments,创建/更新时全量同步附件表 - 前端 Create:form.attachments、URL/文件名解析、上传归一化、编辑省略空 attachments 防误删 - API 增加 ContestAttachmentInput、CreateContestForm.reviewRuleId/attachments Made-with: Cursor
This commit is contained in:
parent
ba93872922
commit
e484fa3965
@ -127,4 +127,30 @@ public class CreateContestDto {
|
|||||||
|
|
||||||
@Schema(description = "结果发布时间")
|
@Schema(description = "结果发布时间")
|
||||||
private String resultPublishTime;
|
private String resultPublishTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动附件全量列表(与 PATCH/POST 对齐)。为 null 时不修改原有附件;非 null(含空列表)则按列表覆盖同步。
|
||||||
|
*/
|
||||||
|
@Schema(description = "活动附件列表:id 为空表示新增,已有 id 表示保留/更新;列表中不出现的已存附件将被删除")
|
||||||
|
private List<AttachmentItem> attachments;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "活动附件项")
|
||||||
|
public static class AttachmentItem {
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,6 +103,9 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
|
|
||||||
save(entity);
|
save(entity);
|
||||||
log.info("活动创建成功,ID:{}, 名称:{}", entity.getId(), entity.getContestName());
|
log.info("活动创建成功,ID:{}, 名称:{}", entity.getId(), entity.getContestName());
|
||||||
|
if (dto.getAttachments() != null) {
|
||||||
|
syncContestAttachments(entity.getId(), dto.getAttachments());
|
||||||
|
}
|
||||||
return entity;
|
return entity;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("创建活动失败,名称:{}", dto.getContestName(), e);
|
log.error("创建活动失败,名称:{}", dto.getContestName(), e);
|
||||||
@ -340,6 +343,9 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
|
|
||||||
mapDtoToEntity(dto, entity);
|
mapDtoToEntity(dto, entity);
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
|
if (dto.getAttachments() != null) {
|
||||||
|
syncContestAttachments(id, dto.getAttachments());
|
||||||
|
}
|
||||||
|
|
||||||
log.info("活动更新成功,ID:{}", id);
|
log.info("活动更新成功,ID:{}", id);
|
||||||
return entity;
|
return entity;
|
||||||
@ -607,6 +613,65 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
|
|
||||||
// ====== 私有辅助方法 ======
|
// ====== 私有辅助方法 ======
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按请求体全量同步活动附件:列表中有 id 的保留并更新 URL/名称;无 id 的新增;库中已有但未出现在列表中的删除。
|
||||||
|
*/
|
||||||
|
private void syncContestAttachments(Long contestId, List<CreateContestDto.AttachmentItem> items) {
|
||||||
|
if (items == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<BizContestAttachment> qw = new LambdaQueryWrapper<>();
|
||||||
|
qw.eq(BizContestAttachment::getContestId, contestId);
|
||||||
|
List<BizContestAttachment> existing = contestAttachmentMapper.selectList(qw);
|
||||||
|
Set<Long> keepIds = items.stream()
|
||||||
|
.map(CreateContestDto.AttachmentItem::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
for (BizContestAttachment row : existing) {
|
||||||
|
if (!keepIds.contains(row.getId())) {
|
||||||
|
contestAttachmentMapper.deleteById(row.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (CreateContestDto.AttachmentItem item : items) {
|
||||||
|
if (!StringUtils.hasText(item.getFileUrl())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String fileName = StringUtils.hasText(item.getFileName()) ? item.getFileName().trim() : "附件";
|
||||||
|
|
||||||
|
if (item.getId() == null) {
|
||||||
|
BizContestAttachment na = new BizContestAttachment();
|
||||||
|
na.setContestId(contestId);
|
||||||
|
na.setFileName(fileName);
|
||||||
|
na.setFileUrl(item.getFileUrl().trim());
|
||||||
|
na.setFormat(item.getFormat());
|
||||||
|
na.setFileType(item.getFileType());
|
||||||
|
na.setSize(item.getSize());
|
||||||
|
na.setValidState(1);
|
||||||
|
contestAttachmentMapper.insert(na);
|
||||||
|
} else {
|
||||||
|
BizContestAttachment a = contestAttachmentMapper.selectById(item.getId());
|
||||||
|
if (a == null || !contestId.equals(a.getContestId())) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
contestAttachmentMapper.updateById(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void mapDtoToEntity(CreateContestDto dto, BizContest entity) {
|
private void mapDtoToEntity(CreateContestDto dto, BizContest entity) {
|
||||||
if (StringUtils.hasText(dto.getContestName())) {
|
if (StringUtils.hasText(dto.getContestName())) {
|
||||||
entity.setContestName(dto.getContestName());
|
entity.setContestName(dto.getContestName());
|
||||||
|
|||||||
@ -108,9 +108,22 @@ export interface CreateContestForm {
|
|||||||
workType?: "image" | "video" | "document" | "code" | "other";
|
workType?: "image" | "video" | "document" | "code" | "other";
|
||||||
workRequirement?: string;
|
workRequirement?: string;
|
||||||
// 评审配置
|
// 评审配置
|
||||||
|
reviewRuleId?: number;
|
||||||
reviewStartTime: string;
|
reviewStartTime: string;
|
||||||
reviewEndTime: string;
|
reviewEndTime: string;
|
||||||
resultPublishTime?: string;
|
resultPublishTime?: string;
|
||||||
|
/** 活动附件全量;与后端 PATCH/POST 对齐,传 null/省略表示不改附件,传数组(含 [])则按列表同步 */
|
||||||
|
attachments?: ContestAttachmentInput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建/更新活动时随主体提交的附件项(与后端 CreateContestDto.AttachmentItem 一致) */
|
||||||
|
export interface ContestAttachmentInput {
|
||||||
|
id?: number;
|
||||||
|
fileName: string;
|
||||||
|
fileUrl: string;
|
||||||
|
format?: string;
|
||||||
|
fileType?: string;
|
||||||
|
size?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateContestForm extends Partial<CreateContestForm> {
|
export interface UpdateContestForm extends Partial<CreateContestForm> {
|
||||||
|
|||||||
@ -135,7 +135,7 @@
|
|||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<a-form-item label="活动附件" name="attachments">
|
<a-form-item label="活动附件" name="attachments">
|
||||||
<a-upload v-model:file-list="attachmentFileList" :before-upload="beforeFileUpload"
|
<a-upload v-model:file-list="attachmentFileList" :before-upload="beforeFileUpload"
|
||||||
:custom-request="handleAttachmentUpload" :max-count="10">
|
:custom-request="handleAttachmentUpload" :max-count="10" @change="handleAttachmentListChange">
|
||||||
<a-button><upload-outlined /> 上传附件</a-button>
|
<a-button><upload-outlined /> 上传附件</a-button>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
<div class="form-hint">最多 10 个文件,每个不超过 30M</div>
|
<div class="form-hint">最多 10 个文件,每个不超过 30M</div>
|
||||||
@ -209,7 +209,7 @@ import type { Dayjs } from "dayjs"
|
|||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { PlusOutlined, UploadOutlined, ArrowLeftOutlined } from "@ant-design/icons-vue"
|
import { PlusOutlined, UploadOutlined, ArrowLeftOutlined } from "@ant-design/icons-vue"
|
||||||
import RichTextEditor from "@/components/RichTextEditor.vue"
|
import RichTextEditor from "@/components/RichTextEditor.vue"
|
||||||
import { contestsApi, attachmentsApi, reviewRulesApi, type CreateContestForm, type Contest } from "@/api/contests"
|
import { contestsApi, attachmentsApi, reviewRulesApi, type CreateContestForm, type Contest, type ContestAttachment, type ContestAttachmentInput } from "@/api/contests"
|
||||||
import { uploadFile } from "@/api/upload"
|
import { uploadFile } from "@/api/upload"
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -222,7 +222,7 @@ const pageLoading = ref(false)
|
|||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
const form = reactive<CreateContestForm & { reviewRuleId?: number }>({
|
const form = reactive<CreateContestForm>({
|
||||||
contestName: "", contestType: "individual", visibility: "public",
|
contestName: "", contestType: "individual", visibility: "public",
|
||||||
targetCities: [] as string[], ageMin: undefined as number | undefined, ageMax: undefined as number | undefined,
|
targetCities: [] as string[], ageMin: undefined as number | undefined, ageMax: undefined as number | undefined,
|
||||||
startTime: "", endTime: "", content: "", coverUrl: "", posterUrl: "",
|
startTime: "", endTime: "", content: "", coverUrl: "", posterUrl: "",
|
||||||
@ -230,7 +230,10 @@ const form = reactive<CreateContestForm & { reviewRuleId?: number }>({
|
|||||||
registerStartTime: "", registerEndTime: "",
|
registerStartTime: "", registerEndTime: "",
|
||||||
requireAudit: false,
|
requireAudit: false,
|
||||||
submitRule: "once", submitStartTime: "", submitEndTime: "",
|
submitRule: "once", submitStartTime: "", submitEndTime: "",
|
||||||
|
reviewRuleId: undefined,
|
||||||
reviewStartTime: "", reviewEndTime: "", resultPublishTime: "",
|
reviewStartTime: "", reviewEndTime: "", resultPublishTime: "",
|
||||||
|
/** 与 name=\"attachments\" 对应;提交前由 buildAttachmentsPayload 写入 */
|
||||||
|
attachments: undefined as ContestAttachmentInput[] | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const timeRange = ref<[Dayjs, Dayjs] | null>(null)
|
const timeRange = ref<[Dayjs, Dayjs] | null>(null)
|
||||||
@ -242,8 +245,72 @@ const resultPublishTime = ref<Dayjs | null>(null)
|
|||||||
const coverFileList = ref<UploadFile[]>([])
|
const coverFileList = ref<UploadFile[]>([])
|
||||||
const posterFileList = ref<UploadFile[]>([])
|
const posterFileList = ref<UploadFile[]>([])
|
||||||
const attachmentFileList = ref<UploadFile[]>([])
|
const attachmentFileList = ref<UploadFile[]>([])
|
||||||
|
/** 编辑进入页时从服务端加载到的附件条数,用于提交时是否携带 attachments 字段 */
|
||||||
|
const initialAttachmentCountAtLoad = ref(0)
|
||||||
const reviewRuleOptions = ref<{ value: number; label: string }[]>([])
|
const reviewRuleOptions = ref<{ value: number; label: string }[]>([])
|
||||||
|
|
||||||
|
function getAttachmentFileUrl(f: UploadFile): string {
|
||||||
|
const r = f.response as { url?: string; data?: { url?: string } } | undefined
|
||||||
|
return (
|
||||||
|
String(f.url || "").trim() ||
|
||||||
|
String(f.thumbUrl || "").trim() ||
|
||||||
|
String(r?.url || "").trim() ||
|
||||||
|
String(r?.data?.url || "").trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttachmentFileName(f: UploadFile): string {
|
||||||
|
const r = f.response as { filename?: string; originalname?: string } | undefined
|
||||||
|
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()
|
||||||
|
: "") ||
|
||||||
|
"附件"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 兼容后端 camelCase / snake_case 及详情嵌套与独立列表接口 */
|
||||||
|
function attachmentToUploadFile(att: ContestAttachment | 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与后端 PATCH/POST 的 attachments 字段对齐,随活动主体一次提交 */
|
||||||
|
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 fetchReviewRules = async () => {
|
const fetchReviewRules = async () => {
|
||||||
try {
|
try {
|
||||||
const rules = await reviewRulesApi.getForSelect()
|
const rules = await reviewRulesApi.getForSelect()
|
||||||
@ -324,25 +391,63 @@ const handlePosterUpload = async (options: any) => {
|
|||||||
} catch (e: any) { onError(e); message.error(e?.response?.data?.message || "海报上传失败") }
|
} catch (e: any) { onError(e); message.error(e?.response?.data?.message || "海报上传失败") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从 response 补全 Upload 条目上的 url/name,避免仅存在 response 时无法写入后端 */
|
||||||
|
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 handleAttachmentUpload = async (options: any) => {
|
const handleAttachmentUpload = async (options: any) => {
|
||||||
const { file, onSuccess, onError } = options
|
const { file, onSuccess, onError } = options
|
||||||
try {
|
try {
|
||||||
const result: any = await uploadFile(file, "contest/attachment")
|
const result = await uploadFile(file as File, "contest/attachment")
|
||||||
const url = result.data?.url || result.url
|
const url = result.url
|
||||||
if (url) {
|
if (!url) throw new Error("无法获取文件地址")
|
||||||
await nextTick()
|
const mergedResponse = { ...result, url }
|
||||||
const idx = attachmentFileList.value.findIndex(f => f.uid === file.uid || f.name === file.name)
|
onSuccess(mergedResponse, file)
|
||||||
if (idx !== -1) {
|
await nextTick()
|
||||||
attachmentFileList.value[idx] = { ...attachmentFileList.value[idx], url, response: result, status: "done" }
|
const idx = attachmentFileList.value.findIndex(
|
||||||
} else {
|
(f) => f.uid === (file as UploadFile).uid,
|
||||||
attachmentFileList.value.push({ uid: file.uid, name: file.name, status: "done", url, response: result })
|
)
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
onSuccess(); message.success("附件上传成功")
|
}
|
||||||
} else throw new Error("无法获取文件地址")
|
normalizeAttachmentFileList()
|
||||||
|
message.success("附件上传成功")
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const idx = attachmentFileList.value.findIndex(f => f.uid === file.uid || f.name === file.name)
|
const idx = attachmentFileList.value.findIndex(
|
||||||
|
(f) => f.uid === (file as UploadFile).uid,
|
||||||
|
)
|
||||||
if (idx !== -1) attachmentFileList.value[idx].status = "error"
|
if (idx !== -1) attachmentFileList.value[idx].status = "error"
|
||||||
onError(e); message.error(e?.response?.data?.message || "附件上传失败")
|
onError(e)
|
||||||
|
message.error(e?.response?.data?.message || "附件上传失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,6 +497,7 @@ const disabledPublishDate = (current: Dayjs | null) => {
|
|||||||
const loadContestData = async () => {
|
const loadContestData = async () => {
|
||||||
if (!contestId.value) return
|
if (!contestId.value) return
|
||||||
pageLoading.value = true
|
pageLoading.value = true
|
||||||
|
initialAttachmentCountAtLoad.value = 0
|
||||||
try {
|
try {
|
||||||
const c = await contestsApi.getDetail(contestId.value)
|
const c = await contestsApi.getDetail(contestId.value)
|
||||||
form.contestName = c.contestName || ""
|
form.contestName = c.contestName || ""
|
||||||
@ -427,9 +533,22 @@ const loadContestData = async () => {
|
|||||||
|
|
||||||
if (c.coverUrl) coverFileList.value = [{ uid: "-1", name: "cover", status: "done", url: c.coverUrl }]
|
if (c.coverUrl) coverFileList.value = [{ uid: "-1", name: "cover", status: "done", url: c.coverUrl }]
|
||||||
if (c.posterUrl) posterFileList.value = [{ uid: "-2", name: "poster", status: "done", url: c.posterUrl }]
|
if (c.posterUrl) posterFileList.value = [{ uid: "-2", name: "poster", status: "done", url: c.posterUrl }]
|
||||||
if (c.attachments?.length) {
|
|
||||||
attachmentFileList.value = c.attachments.map((att: any, i: number) => ({ uid: `-${i + 3}`, name: att.fileName, status: "done", url: att.fileUrl }))
|
attachmentFileList.value = []
|
||||||
|
let list: ContestAttachment[] = []
|
||||||
|
try {
|
||||||
|
list = (await attachmentsApi.getList(contestId.value)) || []
|
||||||
|
} catch {
|
||||||
|
list = []
|
||||||
}
|
}
|
||||||
|
if (!list.length && (c as Contest).attachments?.length) {
|
||||||
|
list = (c as Contest).attachments as ContestAttachment[]
|
||||||
|
}
|
||||||
|
if (list.length) {
|
||||||
|
attachmentFileList.value = list.map((att, i) => attachmentToUploadFile(att, i))
|
||||||
|
}
|
||||||
|
initialAttachmentCountAtLoad.value = list.length
|
||||||
|
form.attachments = buildAttachmentsPayload()
|
||||||
} catch (e: any) { message.error(e?.response?.data?.message || "加载活动数据失败"); router.back() }
|
} catch (e: any) { message.error(e?.response?.data?.message || "加载活动数据失败"); router.back() }
|
||||||
finally { pageLoading.value = false }
|
finally { pageLoading.value = false }
|
||||||
}
|
}
|
||||||
@ -440,6 +559,23 @@ const handleSubmit = async () => {
|
|||||||
await formRef.value?.validate()
|
await formRef.value?.validate()
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
|
|
||||||
|
const payload = buildAttachmentsPayload()
|
||||||
|
form.attachments = payload
|
||||||
|
|
||||||
|
const doneFiles = attachmentFileList.value.filter((f) => f.status === "done")
|
||||||
|
if (doneFiles.length > 0 && payload.length === 0) {
|
||||||
|
message.error("附件已添加但无法解析文件地址,请删除对应项后重新上传")
|
||||||
|
submitLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑且从未有过附件、当前仍为空:省略 attachments,后端 null 不覆盖附件表 */
|
||||||
|
const omitAttachments =
|
||||||
|
isEdit.value &&
|
||||||
|
payload.length === 0 &&
|
||||||
|
attachmentFileList.value.length === 0 &&
|
||||||
|
initialAttachmentCountAtLoad.value === 0
|
||||||
|
|
||||||
const submitData: CreateContestForm = {
|
const submitData: CreateContestForm = {
|
||||||
contestName: form.contestName, contestType: form.contestType, startTime: form.startTime, endTime: form.endTime,
|
contestName: form.contestName, contestType: form.contestType, startTime: form.startTime, endTime: form.endTime,
|
||||||
content: form.content, coverUrl: form.coverUrl, posterUrl: form.posterUrl,
|
content: form.content, coverUrl: form.coverUrl, posterUrl: form.posterUrl,
|
||||||
@ -455,23 +591,15 @@ const handleSubmit = async () => {
|
|||||||
ageMin: form.ageMin,
|
ageMin: form.ageMin,
|
||||||
ageMax: form.ageMax,
|
ageMax: form.ageMax,
|
||||||
}
|
}
|
||||||
|
if (!omitAttachments) {
|
||||||
|
submitData.attachments = payload
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit.value && contestId.value) {
|
if (isEdit.value && contestId.value) {
|
||||||
await contestsApi.update(contestId.value, submitData)
|
await contestsApi.update(contestId.value, submitData)
|
||||||
message.success("保存成功")
|
message.success("保存成功")
|
||||||
} else {
|
} else {
|
||||||
const contest = await contestsApi.create(submitData)
|
await contestsApi.create(submitData)
|
||||||
if (attachmentFileList.value.length > 0) {
|
|
||||||
try {
|
|
||||||
await Promise.all(attachmentFileList.value.map(file => {
|
|
||||||
const fileUrl = file.url || file.response?.url || file.response?.data?.url
|
|
||||||
if (fileUrl && file.name) {
|
|
||||||
return attachmentsApi.create({ contestId: contest.id, fileName: file.name, fileUrl, format: file.name.split(".").pop()?.toLowerCase(), size: file.size?.toString() })
|
|
||||||
}
|
|
||||||
return Promise.resolve()
|
|
||||||
}))
|
|
||||||
} catch { message.warning("活动创建成功,但部分附件记录创建失败") }
|
|
||||||
}
|
|
||||||
message.success("创建成功")
|
message.success("创建成功")
|
||||||
}
|
}
|
||||||
router.push(`/${tenantCode}/contests/list`)
|
router.push(`/${tenantCode}/contests/list`)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user