library-picturebook-activity/frontend/src/views/contests/Create.vue

937 lines
29 KiB
Vue
Raw Normal View History

2026-01-08 09:17:46 +08:00
<template>
<div class="create-contest-page">
2026-01-15 16:35:00 +08:00
<a-spin :spinning="pageLoading">
2026-01-16 16:35:43 +08:00
<a-card>
<template #title>
<a-space>
<a-button
type="text"
@click="handleCancel"
style="padding: 0; margin-right: 8px"
2026-01-15 16:35:00 +08:00
>
2026-01-16 16:35:43 +08:00
<template #icon><ArrowLeftOutlined /></template>
</a-button>
<a-breadcrumb>
<a-breadcrumb-item>
<router-link :to="`/${tenantCode}/contests`"
>活动管理</router-link
2026-01-16 16:35:43 +08:00
>
</a-breadcrumb-item>
<a-breadcrumb-item>{{
isEdit ? "编辑活动" : "创建活动"
2026-01-16 16:35:43 +08:00
}}</a-breadcrumb-item>
</a-breadcrumb>
</a-space>
</template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 19 }"
layout="vertical"
>
<!-- 主办信息 -->
<a-card title="主办信息" size="small" class="section-card">
<a-form-item label="主办单位" name="organizers" required>
<a-input
v-model:value="form.organizers"
placeholder="请输入主办单位"
:maxlength="200"
style="width: 600px"
2026-01-08 09:17:46 +08:00
/>
2026-01-16 16:35:43 +08:00
</a-form-item>
<a-form-item label="协办单位" name="coOrganizers">
<a-input
v-model:value="form.coOrganizers"
placeholder="请输入协办单位"
:maxlength="200"
style="width: 600px"
/>
</a-form-item>
<a-form-item label="赞助单位" name="sponsors">
<a-input
v-model:value="form.sponsors"
placeholder="请输入赞助单位"
:maxlength="200"
style="width: 600px"
/>
</a-form-item>
</a-card>
<!-- 活动信息 -->
<a-card title="活动信息" size="small" class="section-card">
<a-form-item label="活动名称" name="contestName" required>
2026-01-16 16:35:43 +08:00
<a-input
v-model:value="form.contestName"
placeholder="请输入活动名称"
2026-01-16 16:35:43 +08:00
:maxlength="200"
style="width: 600px"
/>
</a-form-item>
<a-form-item label="活动时间" name="timeRange" required>
2026-01-16 16:35:43 +08:00
<a-range-picker
v-model:value="timeRange"
show-time
format="YYYY-MM-DD HH:mm"
style="width: 600px"
@change="handleTimeRangeChange"
/>
</a-form-item>
<a-form-item label="活动类型" name="contestType" required>
2026-01-16 16:35:43 +08:00
<a-select
v-model:value="form.contestType"
placeholder="请选择活动类型"
2026-01-16 16:35:43 +08:00
style="width: 200px"
>
<a-select-option value="individual">个人参与</a-select-option>
<a-select-option value="team">团队参与</a-select-option>
2026-01-16 16:35:43 +08:00
</a-select>
</a-form-item>
<a-form-item label="可见范围" name="visibility">
<a-radio-group v-model:value="form.visibility">
<a-radio value="public">公开所有公众用户可见</a-radio>
<a-radio value="targeted">定向推送按城市/年龄筛选</a-radio>
<a-radio value="designated">指定机构</a-radio>
<a-radio value="internal">仅内部</a-radio>
</a-radio-group>
</a-form-item>
<!-- 定向推送条件 -->
<template v-if="form.visibility === 'targeted'">
<a-form-item label="目标城市">
<a-select
v-model:value="form.targetCities"
mode="tags"
placeholder="输入城市名称后按回车添加,如:广州、深圳"
style="width: 500px"
/>
<template #extra>
<span style="font-size: 12px; color: #9ca3af">留空表示不限城市用户或子女的城市匹配任一目标城市即可看到活动</span>
</template>
</a-form-item>
</template>
<!-- 年龄限制公开和定向都可设置 -->
<a-form-item v-if="form.visibility === 'public' || form.visibility === 'targeted'" label="年龄限制">
<a-space>
<a-input-number v-model:value="form.ageMin" :min="1" :max="99" placeholder="最小" style="width: 100px" />
<span></span>
<a-input-number v-model:value="form.ageMax" :min="1" :max="99" placeholder="最大" style="width: 100px" />
<span style="font-size: 12px; color: #9ca3af">留空表示不限年龄报名时根据参与者生日校验</span>
</a-space>
</a-form-item>
<a-form-item label="活动详情" name="content" required>
2026-01-16 16:35:43 +08:00
<RichTextEditor
v-model="form.content"
placeholder="请输入活动详细说明"
2026-01-16 16:35:43 +08:00
:height="300"
style="width: 800px"
/>
</a-form-item>
<a-form-item label="活动封面" name="coverUrl" required>
2026-01-16 16:35:43 +08:00
<a-upload
v-model:file-list="coverFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="handleCoverUpload"
accept="image/*"
:max-count="1"
>
<div v-if="coverFileList.length < 1">
<plus-outlined />
<div style="margin-top: 8px">上传封面</div>
</div>
</a-upload>
<div class="upload-tip">
<a-alert
message="图片要求尺寸16:9文件大小小于30M"
type="info"
show-icon
style="margin-top: 8px; width: fit-content"
/>
</div>
</a-form-item>
<a-form-item label="活动海报" name="posterUrl" required>
2026-01-16 16:35:43 +08:00
<a-upload
v-model:file-list="posterFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="handlePosterUpload"
accept="image/*"
:max-count="1"
>
<div v-if="posterFileList.length < 1">
<plus-outlined />
<div style="margin-top: 8px">上传海报</div>
</div>
</a-upload>
<div class="upload-tip">
<a-alert
message="图片要求尺寸16:9文件大小小于30M"
type="info"
show-icon
style="margin-top: 8px; width: fit-content"
/>
2026-01-15 16:35:00 +08:00
</div>
2026-01-16 16:35:43 +08:00
</a-form-item>
<a-form-item label="活动附件" name="attachments">
2026-01-16 16:35:43 +08:00
<a-upload
v-model:file-list="attachmentFileList"
:before-upload="beforeFileUpload"
:custom-request="handleAttachmentUpload"
:max-count="10"
>
<a-button>
<upload-outlined />
上传附件
</a-button>
</a-upload>
</a-form-item>
</a-card>
<!-- 报名设置 -->
<a-card title="报名设置" size="small" class="section-card">
<a-form-item label="报名时间" name="registerTimeRange" required>
<a-range-picker
v-model:value="registerTimeRange"
show-time
format="YYYY-MM-DD HH:mm"
style="width: 600px"
:disabled-date="disabledRegisterDate"
@change="handleRegisterTimeRangeChange"
/>
</a-form-item>
</a-card>
<!-- 作品提交设置 -->
<a-card title="作品提交设置" size="small" class="section-card">
<a-form-item label="提交规则" name="submitRule" required>
<a-radio-group v-model:value="form.submitRule">
<a-radio value="once">仅允许提交一次</a-radio>
<a-radio value="resubmit">时间范围内允许重复提交</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="提交时间" name="submitTimeRange" required>
<a-range-picker
v-model:value="submitTimeRange"
show-time
format="YYYY-MM-DD HH:mm"
style="width: 600px"
:disabled-date="disabledSubmitDate"
@change="handleSubmitTimeRangeChange"
/>
</a-form-item>
</a-card>
<!-- 评审规则 -->
<a-card title="评审规则" size="small" class="section-card">
<a-form-item label="评审规则" name="reviewRuleId">
<a-select
v-model:value="form.reviewRuleId"
placeholder="请选择评审规则(可选)"
style="width: 600px"
:options="reviewRuleOptions"
allow-clear
/>
</a-form-item>
<a-form-item label="评审时间" name="reviewTimeRange" required>
<a-range-picker
v-model:value="reviewTimeRange"
show-time
format="YYYY-MM-DD HH:mm"
style="width: 600px"
:disabled-date="disabledReviewDate"
@change="handleReviewTimeRangeChange"
/>
</a-form-item>
</a-card>
<!-- 结果公布 -->
<a-card title="结果公布" size="small" class="section-card">
<a-form-item label="公布时间" name="resultPublishTime">
<a-date-picker
v-model:value="resultPublishTime"
show-time
format="YYYY-MM-DD HH:mm"
style="width: 600px"
:disabled-date="disabledPublishDate"
placeholder="请选择结果公布时间"
@change="handleResultPublishTimeChange"
2026-01-08 09:17:46 +08:00
/>
2026-01-16 16:35:43 +08:00
</a-form-item>
</a-card>
</a-form>
<!-- 底部操作按钮 -->
<div class="form-actions">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button
type="primary"
:loading="submitLoading"
@click="handleSubmit"
2026-01-08 09:17:46 +08:00
>
2026-01-16 16:35:43 +08:00
保存
</a-button>
</a-space>
</div>
</a-card>
2026-01-08 09:17:46 +08:00
</a-spin>
</div>
</template>
<script setup lang="ts">
2026-01-15 16:35:00 +08:00
import { ref, reactive, nextTick, onMounted, computed } from "vue"
2026-01-08 09:17:46 +08:00
import { useRouter, useRoute } from "vue-router"
import { message } from "ant-design-vue"
import type { FormInstance, UploadFile } from "ant-design-vue"
import type { Dayjs } from "dayjs"
2026-01-15 16:35:00 +08:00
import dayjs from "dayjs"
2026-01-08 09:17:46 +08:00
import {
PlusOutlined,
UploadOutlined,
ArrowLeftOutlined,
} from "@ant-design/icons-vue"
2026-01-15 16:35:00 +08:00
import RichTextEditor from "@/components/RichTextEditor.vue"
2026-01-08 09:17:46 +08:00
import {
contestsApi,
2026-01-15 16:35:00 +08:00
attachmentsApi,
2026-01-08 09:17:46 +08:00
reviewRulesApi,
type CreateContestForm,
2026-01-15 16:35:00 +08:00
type Contest,
2026-01-08 09:17:46 +08:00
} from "@/api/contests"
2026-01-15 16:35:00 +08:00
import { uploadFile } from "@/api/upload"
2026-01-08 09:17:46 +08:00
const router = useRouter()
const route = useRoute()
const tenantCode = route.params.tenantCode as string
2026-01-15 16:35:00 +08:00
// 编辑模式
2026-01-16 16:35:43 +08:00
const contestId = computed(() =>
route.params.id ? Number(route.params.id) : null
)
2026-01-15 16:35:00 +08:00
const isEdit = computed(() => !!contestId.value)
const pageLoading = ref(false)
2026-01-08 09:17:46 +08:00
const formRef = ref<FormInstance>()
const submitLoading = ref(false)
// 表单数据
const form = reactive<
CreateContestForm & {
reviewRuleId?: number
}
>({
contestName: "",
contestType: "individual",
visibility: "designated",
targetCities: [] as string[],
ageMin: undefined as number | undefined,
ageMax: undefined as number | undefined,
2026-01-08 09:17:46 +08:00
startTime: "",
endTime: "",
content: "",
coverUrl: "",
posterUrl: "",
organizers: "",
coOrganizers: "",
sponsors: "",
registerStartTime: "",
registerEndTime: "",
submitRule: "once",
submitStartTime: "",
submitEndTime: "",
reviewStartTime: "",
reviewEndTime: "",
resultPublishTime: "",
})
// 时间范围
const timeRange = ref<[Dayjs, Dayjs] | null>(null)
const registerTimeRange = ref<[Dayjs, Dayjs] | null>(null)
const submitTimeRange = ref<[Dayjs, Dayjs] | null>(null)
const reviewTimeRange = ref<[Dayjs, Dayjs] | null>(null)
const resultPublishTime = ref<Dayjs | null>(null)
// 文件列表
const coverFileList = ref<UploadFile[]>([])
const posterFileList = ref<UploadFile[]>([])
const attachmentFileList = ref<UploadFile[]>([])
// 评审规则选项
const reviewRuleOptions = ref<{ value: number; label: string }[]>([])
2026-01-15 16:35:00 +08:00
// 获取评审规则列表
const fetchReviewRules = async () => {
try {
const rules = await reviewRulesApi.getForSelect()
2026-01-16 16:35:43 +08:00
reviewRuleOptions.value = rules.map((rule) => ({
2026-01-15 16:35:00 +08:00
value: rule.id,
label: rule.ruleName,
}))
} catch (error) {
2026-01-16 16:35:43 +08:00
console.error("获取评审规则列表失败:", error)
2026-01-15 16:35:00 +08:00
}
}
2026-01-08 09:17:46 +08:00
// 表单验证规则
const rules = {
contestName: [{ required: true, message: "请输入活动名称", trigger: "blur" }],
2026-01-08 09:17:46 +08:00
contestType: [
{ required: true, message: "请选择活动类型", trigger: "change" },
2026-01-08 09:17:46 +08:00
],
organizers: [
{ required: true, message: "请输入主办单位", trigger: "change" },
],
content: [{ required: true, message: "请输入活动详情", trigger: "blur" }],
coverUrl: [{ required: true, message: "请上传活动封面", trigger: "change" }],
posterUrl: [{ required: true, message: "请上传活动海报", trigger: "change" }],
2026-01-08 09:17:46 +08:00
timeRange: [
{
required: true,
validator: () => {
if (!timeRange.value || !form.startTime || !form.endTime) {
return Promise.reject(new Error("请选择活动时间"))
2026-01-08 09:17:46 +08:00
}
return Promise.resolve()
},
trigger: "change",
},
],
registerTimeRange: [
{
required: true,
validator: () => {
if (
!registerTimeRange.value ||
!form.registerStartTime ||
!form.registerEndTime
) {
return Promise.reject(new Error("请选择报名时间"))
}
return Promise.resolve()
},
trigger: "change",
},
],
submitRule: [
{ required: true, message: "请选择提交规则", trigger: "change" },
],
submitTimeRange: [
{
required: true,
validator: () => {
if (
!submitTimeRange.value ||
!form.submitStartTime ||
!form.submitEndTime
) {
return Promise.reject(new Error("请选择提交时间"))
}
return Promise.resolve()
},
trigger: "change",
},
],
reviewTimeRange: [
{
required: true,
validator: () => {
if (
!reviewTimeRange.value ||
!form.reviewStartTime ||
!form.reviewEndTime
) {
return Promise.reject(new Error("请选择评审时间"))
}
return Promise.resolve()
},
trigger: "change",
},
],
}
// 图片上传前验证
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith("image/")
if (!isImage) {
message.error("只能上传图片文件!")
return false
}
const isLt30M = file.size / 1024 / 1024 < 30
if (!isLt30M) {
message.error("图片大小不能超过30M")
return false
}
// 验证图片尺寸16:9
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const aspectRatio = img.width / img.height
const targetRatio = 16 / 9
const tolerance = 0.1 // 允许10%的误差
if (Math.abs(aspectRatio - targetRatio) > tolerance) {
message.warning("图片尺寸应为16:9当前尺寸可能不符合要求")
}
resolve(true)
}
img.src = e.target?.result as string
}
reader.readAsDataURL(file)
})
}
// 文件上传前验证
const beforeFileUpload = (file: File) => {
const isLt30M = file.size / 1024 / 1024 < 30
if (!isLt30M) {
message.error("文件大小不能超过30M")
return false
}
return true
}
// 封面图片上传
const handleCoverUpload = async (options: any) => {
const { file, onSuccess, onError } = options
try {
2026-01-15 16:35:00 +08:00
const result: any = await uploadFile(file)
// 兼容不同的响应格式
const url = result.data?.url || result.url
if (url) {
form.coverUrl = url
onSuccess()
message.success("封面上传成功")
} else {
throw new Error("无法获取图片地址")
}
} catch (error: any) {
console.error("封面上传失败:", error)
2026-01-08 09:17:46 +08:00
onError(error)
2026-01-15 16:35:00 +08:00
message.error(error?.response?.data?.message || "封面上传失败")
2026-01-08 09:17:46 +08:00
}
}
// 海报图片上传
const handlePosterUpload = async (options: any) => {
const { file, onSuccess, onError } = options
try {
2026-01-15 16:35:00 +08:00
const result: any = await uploadFile(file)
// 兼容不同的响应格式
const url = result.data?.url || result.url
if (url) {
form.posterUrl = url
onSuccess()
message.success("海报上传成功")
} else {
throw new Error("无法获取图片地址")
}
} catch (error: any) {
console.error("海报上传失败:", error)
2026-01-08 09:17:46 +08:00
onError(error)
2026-01-15 16:35:00 +08:00
message.error(error?.response?.data?.message || "海报上传失败")
2026-01-08 09:17:46 +08:00
}
}
// 附件上传
const handleAttachmentUpload = async (options: any) => {
2026-01-15 16:35:00 +08:00
const { file, onSuccess, onError } = options
2026-01-08 09:17:46 +08:00
try {
2026-01-15 16:35:00 +08:00
const result: any = await uploadFile(file)
// 兼容不同的响应格式
const url = result.data?.url || result.url
if (url) {
// 更新文件列表中的文件信息保存上传后的URL
// 使用 nextTick 确保文件已添加到列表中
await nextTick()
const fileIndex = attachmentFileList.value.findIndex(
(f) => f.uid === file.uid || f.name === file.name
)
if (fileIndex !== -1) {
attachmentFileList.value[fileIndex] = {
...attachmentFileList.value[fileIndex],
url,
response: result,
status: "done",
}
} else {
// 如果找不到,手动添加到列表
attachmentFileList.value.push({
uid: file.uid,
name: file.name,
status: "done",
url,
response: result,
})
}
onSuccess()
message.success("附件上传成功")
} else {
throw new Error("无法获取文件地址")
}
} catch (error: any) {
console.error("附件上传失败:", error)
// 标记文件为错误状态
const fileIndex = attachmentFileList.value.findIndex(
(f) => f.uid === file.uid || f.name === file.name
)
if (fileIndex !== -1) {
attachmentFileList.value[fileIndex].status = "error"
}
2026-01-08 09:17:46 +08:00
onError(error)
2026-01-15 16:35:00 +08:00
message.error(error?.response?.data?.message || "附件上传失败")
2026-01-08 09:17:46 +08:00
}
}
// 时间范围变化处理
const handleTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
if (dates && dates.length === 2) {
form.startTime = dates[0].format("YYYY-MM-DD HH:mm:ss")
form.endTime = dates[1].format("YYYY-MM-DD HH:mm:ss")
} else {
form.startTime = ""
form.endTime = ""
}
// 触发验证
formRef.value?.validateFields(["timeRange"])
}
const handleRegisterTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
if (dates && dates.length === 2) {
form.registerStartTime = dates[0].format("YYYY-MM-DD HH:mm:ss")
form.registerEndTime = dates[1].format("YYYY-MM-DD HH:mm:ss")
} else {
form.registerStartTime = ""
form.registerEndTime = ""
}
// 触发验证
formRef.value?.validateFields(["registerTimeRange"])
}
const handleSubmitTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
if (dates && dates.length === 2) {
form.submitStartTime = dates[0].format("YYYY-MM-DD HH:mm:ss")
form.submitEndTime = dates[1].format("YYYY-MM-DD HH:mm:ss")
} else {
form.submitStartTime = ""
form.submitEndTime = ""
}
// 触发验证
formRef.value?.validateFields(["submitTimeRange"])
}
const handleReviewTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
if (dates && dates.length === 2) {
form.reviewStartTime = dates[0].format("YYYY-MM-DD HH:mm:ss")
form.reviewEndTime = dates[1].format("YYYY-MM-DD HH:mm:ss")
} else {
form.reviewStartTime = ""
form.reviewEndTime = ""
}
// 触发验证
formRef.value?.validateFields(["reviewTimeRange"])
}
const handleResultPublishTimeChange = (date: Dayjs | null) => {
if (date) {
form.resultPublishTime = date.format("YYYY-MM-DD HH:mm:ss")
} else {
form.resultPublishTime = ""
}
}
// 禁用报名日期(不早于活动开始时间,不晚于活动结束时间)
2026-01-08 09:17:46 +08:00
const disabledRegisterDate = (current: Dayjs | null) => {
if (!timeRange.value || !current) return false
const [startTime, endTime] = timeRange.value
return current < startTime.startOf("day") || current > endTime.endOf("day")
}
// 禁用提交日期(不早于报名结束时间,不晚于活动结束时间)
2026-01-08 09:17:46 +08:00
const disabledSubmitDate = (current: Dayjs | null) => {
if (!registerTimeRange.value || !timeRange.value || !current) return false
const registerEndTime = registerTimeRange.value[1]
const [, endTime] = timeRange.value
return (
current < registerEndTime.startOf("day") || current > endTime.endOf("day")
)
}
// 禁用评审日期(不早于报名结束时间,不晚于活动结束时间)
2026-01-08 09:17:46 +08:00
const disabledReviewDate = (current: Dayjs | null) => {
if (!registerTimeRange.value || !timeRange.value || !current) return false
const registerEndTime = registerTimeRange.value[1]
const [, endTime] = timeRange.value
return (
current < registerEndTime.startOf("day") || current > endTime.endOf("day")
)
}
// 禁用公布日期(不早于评审结束时间,不晚于活动结束时间)
2026-01-08 09:17:46 +08:00
const disabledPublishDate = (current: Dayjs | null) => {
if (!reviewTimeRange.value || !timeRange.value || !current) return false
const reviewEndTime = reviewTimeRange.value[1]
const [, endTime] = timeRange.value
return (
current < reviewEndTime.startOf("day") || current > endTime.endOf("day")
)
}
// 加载活动数据(编辑模式)
2026-01-15 16:35:00 +08:00
const loadContestData = async () => {
if (!contestId.value) return
2026-01-08 09:17:46 +08:00
2026-01-15 16:35:00 +08:00
pageLoading.value = true
2026-01-08 09:17:46 +08:00
try {
2026-01-15 16:35:00 +08:00
const contest = await contestsApi.getDetail(contestId.value)
2026-01-08 09:17:46 +08:00
// 填充表单数据
2026-01-16 16:35:43 +08:00
form.contestName = contest.contestName || ""
form.contestType = contest.contestType || "individual"
form.startTime = contest.startTime || ""
form.endTime = contest.endTime || ""
form.content = contest.content || ""
form.coverUrl = contest.coverUrl || ""
form.posterUrl = contest.posterUrl || ""
2026-01-15 16:35:00 +08:00
// 处理主办/协办/赞助单位(后端返回数组,表单需要字符串)
2026-01-08 09:17:46 +08:00
form.organizers = Array.isArray(contest.organizers)
2026-01-16 16:35:43 +08:00
? contest.organizers.join("、")
: contest.organizers || ""
2026-01-08 09:17:46 +08:00
form.coOrganizers = Array.isArray(contest.coOrganizers)
2026-01-16 16:35:43 +08:00
? contest.coOrganizers.join("、")
: contest.coOrganizers || ""
2026-01-08 09:17:46 +08:00
form.sponsors = Array.isArray(contest.sponsors)
2026-01-16 16:35:43 +08:00
? contest.sponsors.join("、")
: contest.sponsors || ""
form.registerStartTime = contest.registerStartTime || ""
form.registerEndTime = contest.registerEndTime || ""
form.submitRule = contest.submitRule || "once"
form.submitStartTime = contest.submitStartTime || ""
form.submitEndTime = contest.submitEndTime || ""
2026-01-15 16:35:00 +08:00
form.reviewRuleId = contest.reviewRuleId || undefined
2026-01-16 16:35:43 +08:00
form.reviewStartTime = contest.reviewStartTime || ""
form.reviewEndTime = contest.reviewEndTime || ""
form.resultPublishTime = contest.resultPublishTime || ""
2026-01-08 09:17:46 +08:00
// 设置时间范围
if (contest.startTime && contest.endTime) {
timeRange.value = [dayjs(contest.startTime), dayjs(contest.endTime)]
}
if (contest.registerStartTime && contest.registerEndTime) {
2026-01-16 16:35:43 +08:00
registerTimeRange.value = [
dayjs(contest.registerStartTime),
dayjs(contest.registerEndTime),
]
2026-01-08 09:17:46 +08:00
}
if (contest.submitStartTime && contest.submitEndTime) {
2026-01-16 16:35:43 +08:00
submitTimeRange.value = [
dayjs(contest.submitStartTime),
dayjs(contest.submitEndTime),
]
2026-01-08 09:17:46 +08:00
}
if (contest.reviewStartTime && contest.reviewEndTime) {
2026-01-16 16:35:43 +08:00
reviewTimeRange.value = [
dayjs(contest.reviewStartTime),
dayjs(contest.reviewEndTime),
]
2026-01-08 09:17:46 +08:00
}
if (contest.resultPublishTime) {
resultPublishTime.value = dayjs(contest.resultPublishTime)
}
2026-01-15 16:35:00 +08:00
// 设置封面图片
2026-01-08 09:17:46 +08:00
if (contest.coverUrl) {
2026-01-16 16:35:43 +08:00
coverFileList.value = [
{
uid: "-1",
name: "cover",
status: "done",
url: contest.coverUrl,
},
]
2026-01-08 09:17:46 +08:00
}
2026-01-15 16:35:00 +08:00
// 设置海报图片
2026-01-08 09:17:46 +08:00
if (contest.posterUrl) {
2026-01-16 16:35:43 +08:00
posterFileList.value = [
{
uid: "-2",
name: "poster",
status: "done",
url: contest.posterUrl,
},
]
2026-01-15 16:35:00 +08:00
}
// 加载附件
if (contest.attachments && contest.attachments.length > 0) {
2026-01-16 16:35:43 +08:00
attachmentFileList.value = contest.attachments.map(
(att: any, index: number) => ({
uid: `-${index + 3}`,
name: att.fileName,
status: "done",
url: att.fileUrl,
})
)
2026-01-08 09:17:46 +08:00
}
} catch (error: any) {
message.error(error?.response?.data?.message || "加载活动数据失败")
2026-01-15 16:35:00 +08:00
router.back()
2026-01-08 09:17:46 +08:00
} finally {
2026-01-15 16:35:00 +08:00
pageLoading.value = false
2026-01-08 09:17:46 +08:00
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value?.validate()
submitLoading.value = true
2026-01-15 16:35:00 +08:00
// 构建提交数据,确保所有字符串字段不为 null/undefined
const submitData: CreateContestForm = {
2026-01-16 16:35:43 +08:00
contestName: form.contestName || "",
contestType: form.contestType || "individual",
startTime: form.startTime || "",
endTime: form.endTime || "",
content: form.content || "",
coverUrl: form.coverUrl || "",
posterUrl: form.posterUrl || "",
organizers: form.organizers || "",
coOrganizers: form.coOrganizers || "",
sponsors: form.sponsors || "",
registerStartTime: form.registerStartTime || "",
registerEndTime: form.registerEndTime || "",
submitRule: form.submitRule || "once",
submitStartTime: form.submitStartTime || "",
submitEndTime: form.submitEndTime || "",
2026-01-15 16:35:00 +08:00
reviewRuleId: form.reviewRuleId || undefined,
2026-01-16 16:35:43 +08:00
reviewStartTime: form.reviewStartTime || "",
reviewEndTime: form.reviewEndTime || "",
2026-01-15 16:35:00 +08:00
resultPublishTime: form.resultPublishTime || undefined,
}
if (isEdit.value && contestId.value) {
// 编辑模式 - 更新活动
2026-01-15 16:35:00 +08:00
await contestsApi.update(contestId.value, submitData)
message.success("保存成功")
2026-01-08 09:17:46 +08:00
} else {
2026-01-15 16:35:00 +08:00
// 创建模式
const contest = await contestsApi.create(submitData)
const newContestId = contest.id
// 如果有附件,创建附件记录
if (attachmentFileList.value.length > 0) {
try {
const attachmentPromises = attachmentFileList.value.map((file) => {
const fileUrl =
file.url || file.response?.url || file.response?.data?.url
if (fileUrl && file.name) {
return attachmentsApi.create({
contestId: newContestId,
fileName: file.name,
fileUrl,
format: file.name.split(".").pop()?.toLowerCase(),
size: file.size?.toString(),
})
}
return Promise.resolve()
})
await Promise.all(attachmentPromises)
} catch (attachmentError) {
console.error("创建附件记录失败:", attachmentError)
message.warning("活动创建成功,但部分附件记录创建失败")
2026-01-15 16:35:00 +08:00
}
}
2026-01-08 09:17:46 +08:00
message.success("创建成功")
}
// 跳转到活动列表
2026-01-08 09:17:46 +08:00
router.push(`/${tenantCode}/contests`)
} catch (error: any) {
if (error?.errorFields) {
// 表单验证错误
return
}
2026-01-16 16:35:43 +08:00
message.error(
error?.response?.data?.message || (isEdit.value ? "保存失败" : "创建失败")
)
2026-01-08 09:17:46 +08:00
} finally {
submitLoading.value = false
}
}
// 取消
const handleCancel = () => {
router.back()
}
2026-01-15 16:35:00 +08:00
// 页面加载
onMounted(() => {
// 获取评审规则列表
fetchReviewRules()
2026-01-08 09:17:46 +08:00
2026-01-15 16:35:00 +08:00
if (isEdit.value) {
loadContestData()
2026-01-08 09:17:46 +08:00
}
})
</script>
<style scoped>
.create-contest-page {
padding: 16px 24px;
max-width: 1200px;
2026-01-16 16:35:43 +08:00
background-color: #fff;
2026-01-08 09:17:46 +08:00
}
.create-contest-page :deep(.ant-card) {
border: none;
box-shadow: none;
}
.create-contest-page :deep(.ant-card-head) {
padding: 12px 0;
min-height: auto;
}
.create-contest-page :deep(.ant-card-body) {
padding: 16px 0;
}
.section-card {
margin-bottom: 24px;
border: none;
box-shadow: none;
background: transparent;
}
.section-card :deep(.ant-card-head) {
border-bottom: none;
padding-bottom: 8px;
background: transparent;
}
.section-card :deep(.ant-card-body) {
padding-top: 0;
background: transparent;
}
.section-card :deep(.ant-card-head-title) {
font-weight: bold;
font-size: 16px;
}
.upload-tip {
margin-top: 8px;
}
.form-actions {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
text-align: right;
}
</style>