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-03-31 20:02:24 +08:00
|
|
|
|
<!-- 顶部导航 -->
|
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<a-button type="text" @click="handleCancel" style="padding: 0; margin-right: 8px">
|
|
|
|
|
|
<template #icon><ArrowLeftOutlined /></template>
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-breadcrumb>
|
|
|
|
|
|
<a-breadcrumb-item><router-link :to="`/${tenantCode}/contests`">活动管理</router-link></a-breadcrumb-item>
|
|
|
|
|
|
<a-breadcrumb-item>{{ isEdit ? '编辑活动' : '创建活动' }}</a-breadcrumb-item>
|
|
|
|
|
|
</a-breadcrumb>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical" class="contest-form">
|
|
|
|
|
|
<!-- 主办信息 -->
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3 class="section-title">主办信息</h3>
|
|
|
|
|
|
<a-row :gutter="24">
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="主办单位" name="organizers" required>
|
|
|
|
|
|
<a-input v-model:value="form.organizers" placeholder="请输入主办单位" :maxlength="200" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="协办单位" name="coOrganizers">
|
|
|
|
|
|
<a-input v-model:value="form.coOrganizers" placeholder="请输入协办单位" :maxlength="200" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="赞助单位" name="sponsors">
|
|
|
|
|
|
<a-input v-model:value="form.sponsors" placeholder="请输入赞助单位" :maxlength="200" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 活动信息 -->
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3 class="section-title">活动信息</h3>
|
|
|
|
|
|
<a-row :gutter="24">
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="活动名称" name="contestName" required>
|
|
|
|
|
|
<a-input v-model:value="form.contestName" placeholder="请输入活动名称" :maxlength="200" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="活动类型" name="contestType" required>
|
|
|
|
|
|
<a-select v-model:value="form.contestType" placeholder="请选择活动类型">
|
|
|
|
|
|
<a-select-option value="individual">个人参与</a-select-option>
|
|
|
|
|
|
<a-select-option value="team">团队参与</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="活动时间" name="timeRange" required>
|
|
|
|
|
|
<a-range-picker v-model:value="timeRange" show-time format="YYYY-MM-DD HH:mm" style="width: 100%" @change="handleTimeRangeChange" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="form.visibility === 'targeted'" :span="12">
|
2026-03-27 22:20:25 +08:00
|
|
|
|
<a-form-item label="目标城市">
|
2026-03-31 20:02:24 +08:00
|
|
|
|
<a-select v-model:value="form.targetCities" mode="tags" placeholder="输入城市名称后按回车添加" />
|
|
|
|
|
|
<div class="form-hint">留空表示不限城市</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="form.visibility === 'public' || form.visibility === 'targeted'" :span="12">
|
|
|
|
|
|
<a-form-item 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 class="form-hint">岁(留空不限)</span>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="活动详情" name="content" required>
|
|
|
|
|
|
<RichTextEditor v-model="form.content" placeholder="请输入活动详细说明" :height="300" />
|
2026-03-27 22:20:25 +08:00
|
|
|
|
</a-form-item>
|
2026-03-31 20:02:24 +08:00
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 图片与附件 -->
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3 class="section-title">图片与附件</h3>
|
|
|
|
|
|
<a-row :gutter="24">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="活动封面" name="coverUrl" required>
|
|
|
|
|
|
<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="form-hint">建议尺寸 16:9,小于 30M</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="活动海报" name="posterUrl" required>
|
|
|
|
|
|
<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="form-hint">建议尺寸 16:9,小于 30M</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="活动附件" name="attachments">
|
|
|
|
|
|
<a-upload v-model:file-list="attachmentFileList" :before-upload="beforeFileUpload"
|
|
|
|
|
|
:custom-request="handleAttachmentUpload" :max-count="10">
|
|
|
|
|
|
<a-button><upload-outlined /> 上传附件</a-button>
|
|
|
|
|
|
</a-upload>
|
|
|
|
|
|
<div class="form-hint">最多 10 个文件,每个不超过 30M</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 时间配置 -->
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3 class="section-title">时间配置</h3>
|
|
|
|
|
|
<a-row :gutter="24">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="报名时间" name="registerTimeRange" required>
|
|
|
|
|
|
<a-range-picker v-model:value="registerTimeRange" show-time format="YYYY-MM-DD HH:mm" style="width: 100%"
|
|
|
|
|
|
:disabled-date="disabledRegisterDate" @change="handleRegisterTimeRangeChange" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<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-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="作品提交时间" name="submitTimeRange" required>
|
|
|
|
|
|
<a-range-picker v-model:value="submitTimeRange" show-time format="YYYY-MM-DD HH:mm" style="width: 100%"
|
|
|
|
|
|
:disabled-date="disabledSubmitDate" @change="handleSubmitTimeRangeChange" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="评审规则" name="reviewRuleId">
|
|
|
|
|
|
<a-select v-model:value="form.reviewRuleId" placeholder="请选择评审规则(可选)" :options="reviewRuleOptions" allow-clear />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="评审时间" name="reviewTimeRange" required>
|
|
|
|
|
|
<a-range-picker v-model:value="reviewTimeRange" show-time format="YYYY-MM-DD HH:mm" style="width: 100%"
|
|
|
|
|
|
:disabled-date="disabledReviewDate" @change="handleReviewTimeRangeChange" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="结果公布时间" name="resultPublishTime">
|
|
|
|
|
|
<a-date-picker v-model:value="resultPublishTime" show-time format="YYYY-MM-DD HH:mm" style="width: 100%"
|
|
|
|
|
|
:disabled-date="disabledPublishDate" placeholder="请选择结果公布时间" @change="handleResultPublishTimeChange" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
</div>
|
2026-01-16 16:35:43 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 底部操作按钮 -->
|
|
|
|
|
|
<div class="form-actions">
|
2026-03-31 20:02:24 +08:00
|
|
|
|
<a-button @click="handleCancel">取消</a-button>
|
|
|
|
|
|
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</a-button>
|
2026-01-16 16:35:43 +08:00
|
|
|
|
</div>
|
2026-03-31 20:02:24 +08:00
|
|
|
|
</a-form>
|
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-03-31 20:02:24 +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-03-31 20:02:24 +08:00
|
|
|
|
import { contestsApi, attachmentsApi, reviewRulesApi, type CreateContestForm, type Contest } 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-03-31 20:02:24 +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)
|
|
|
|
|
|
|
2026-03-31 20:02:24 +08:00
|
|
|
|
const form = reactive<CreateContestForm & { reviewRuleId?: number }>({
|
|
|
|
|
|
contestName: "", contestType: "individual", visibility: "designated",
|
|
|
|
|
|
targetCities: [] as string[], ageMin: undefined as number | undefined, ageMax: undefined as number | undefined,
|
|
|
|
|
|
startTime: "", endTime: "", content: "", coverUrl: "", posterUrl: "",
|
|
|
|
|
|
organizers: "", coOrganizers: "", sponsors: "",
|
|
|
|
|
|
registerStartTime: "", registerEndTime: "",
|
|
|
|
|
|
submitRule: "once", submitStartTime: "", submitEndTime: "",
|
|
|
|
|
|
reviewStartTime: "", reviewEndTime: "", resultPublishTime: "",
|
2026-01-08 09:17:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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-03-31 20:02:24 +08:00
|
|
|
|
reviewRuleOptions.value = rules.map(r => ({ value: r.id, label: r.ruleName }))
|
|
|
|
|
|
} catch { /* */ }
|
2026-01-15 16:35:00 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
|
|
|
|
|
const rules = {
|
2026-03-27 22:20:25 +08:00
|
|
|
|
contestName: [{ required: true, message: "请输入活动名称", trigger: "blur" }],
|
2026-03-31 20:02:24 +08:00
|
|
|
|
contestType: [{ required: true, message: "请选择活动类型", trigger: "change" }],
|
|
|
|
|
|
organizers: [{ required: true, message: "请输入主办单位", trigger: "change" }],
|
2026-03-27 22:20:25 +08:00
|
|
|
|
content: [{ required: true, message: "请输入活动详情", trigger: "blur" }],
|
|
|
|
|
|
coverUrl: [{ required: true, message: "请上传活动封面", trigger: "change" }],
|
|
|
|
|
|
posterUrl: [{ required: true, message: "请上传活动海报", trigger: "change" }],
|
2026-03-31 20:02:24 +08:00
|
|
|
|
timeRange: [{ required: true, validator: () => (!timeRange.value || !form.startTime || !form.endTime) ? Promise.reject("请选择活动时间") : Promise.resolve(), trigger: "change" }],
|
|
|
|
|
|
registerTimeRange: [{ required: true, validator: () => (!registerTimeRange.value || !form.registerStartTime || !form.registerEndTime) ? Promise.reject("请选择报名时间") : Promise.resolve(), trigger: "change" }],
|
|
|
|
|
|
submitRule: [{ required: true, message: "请选择提交规则", trigger: "change" }],
|
|
|
|
|
|
submitTimeRange: [{ required: true, validator: () => (!submitTimeRange.value || !form.submitStartTime || !form.submitEndTime) ? Promise.reject("请选择提交时间") : Promise.resolve(), trigger: "change" }],
|
|
|
|
|
|
reviewTimeRange: [{ required: true, validator: () => (!reviewTimeRange.value || !form.reviewStartTime || !form.reviewEndTime) ? Promise.reject("请选择评审时间") : Promise.resolve(), trigger: "change" }],
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const beforeUpload = (file: File) => {
|
|
|
|
|
|
const isImage = file.type.startsWith("image/")
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (!isImage) { message.error("只能上传图片文件!"); return false }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const isLt30M = file.size / 1024 / 1024 < 30
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (!isLt30M) { message.error("图片大小不能超过30M!"); return false }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
const reader = new FileReader()
|
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
|
const img = new Image()
|
|
|
|
|
|
img.onload = () => {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
const ratio = img.width / img.height
|
|
|
|
|
|
if (Math.abs(ratio - 16 / 9) > 0.1) message.warning("图片尺寸建议为16:9")
|
2026-01-08 09:17:46 +08:00
|
|
|
|
resolve(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
img.src = e.target?.result as string
|
|
|
|
|
|
}
|
|
|
|
|
|
reader.readAsDataURL(file)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const beforeFileUpload = (file: File) => {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (file.size / 1024 / 1024 >= 30) { message.error("文件大小不能超过30M!"); return false }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
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
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (url) { form.coverUrl = url; onSuccess(); message.success("封面上传成功") }
|
|
|
|
|
|
else throw new Error("无法获取图片地址")
|
|
|
|
|
|
} catch (e: any) { onError(e); message.error(e?.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
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (url) { form.posterUrl = url; onSuccess(); message.success("海报上传成功") }
|
|
|
|
|
|
else throw new Error("无法获取图片地址")
|
|
|
|
|
|
} catch (e: any) { onError(e); message.error(e?.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) {
|
|
|
|
|
|
await nextTick()
|
2026-03-31 20:02:24 +08:00
|
|
|
|
const idx = attachmentFileList.value.findIndex(f => f.uid === file.uid || f.name === file.name)
|
|
|
|
|
|
if (idx !== -1) {
|
|
|
|
|
|
attachmentFileList.value[idx] = { ...attachmentFileList.value[idx], url, response: result, status: "done" }
|
2026-01-15 16:35:00 +08:00
|
|
|
|
} else {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
attachmentFileList.value.push({ uid: file.uid, name: file.name, status: "done", url, response: result })
|
2026-01-15 16:35:00 +08:00
|
|
|
|
}
|
2026-03-31 20:02:24 +08:00
|
|
|
|
onSuccess(); message.success("附件上传成功")
|
|
|
|
|
|
} else throw new Error("无法获取文件地址")
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const idx = attachmentFileList.value.findIndex(f => f.uid === file.uid || f.name === file.name)
|
|
|
|
|
|
if (idx !== -1) attachmentFileList.value[idx].status = "error"
|
|
|
|
|
|
onError(e); message.error(e?.response?.data?.message || "附件上传失败")
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (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 = "" }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
formRef.value?.validateFields(["timeRange"])
|
|
|
|
|
|
}
|
|
|
|
|
|
const handleRegisterTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (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 = "" }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
formRef.value?.validateFields(["registerTimeRange"])
|
|
|
|
|
|
}
|
|
|
|
|
|
const handleSubmitTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (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 = "" }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
formRef.value?.validateFields(["submitTimeRange"])
|
|
|
|
|
|
}
|
|
|
|
|
|
const handleReviewTimeRangeChange = (dates: [Dayjs, Dayjs] | null) => {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (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 = "" }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
formRef.value?.validateFields(["reviewTimeRange"])
|
|
|
|
|
|
}
|
|
|
|
|
|
const handleResultPublishTimeChange = (date: Dayjs | null) => {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
form.resultPublishTime = date ? date.format("YYYY-MM-DD HH:mm:ss") : ""
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const disabledRegisterDate = (current: Dayjs | null) => {
|
|
|
|
|
|
if (!timeRange.value || !current) return false
|
2026-03-31 20:02:24 +08:00
|
|
|
|
return current < timeRange.value[0].startOf("day") || current > timeRange.value[1].endOf("day")
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
const disabledSubmitDate = (current: Dayjs | null) => {
|
|
|
|
|
|
if (!registerTimeRange.value || !timeRange.value || !current) return false
|
2026-03-31 20:02:24 +08:00
|
|
|
|
return current < registerTimeRange.value[1].startOf("day") || current > timeRange.value[1].endOf("day")
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
const disabledReviewDate = (current: Dayjs | null) => {
|
|
|
|
|
|
if (!registerTimeRange.value || !timeRange.value || !current) return false
|
2026-03-31 20:02:24 +08:00
|
|
|
|
return current < registerTimeRange.value[1].startOf("day") || current > timeRange.value[1].endOf("day")
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
const disabledPublishDate = (current: Dayjs | null) => {
|
|
|
|
|
|
if (!reviewTimeRange.value || !timeRange.value || !current) return false
|
2026-03-31 20:02:24 +08:00
|
|
|
|
return current < reviewTimeRange.value[1].startOf("day") || current > timeRange.value[1].endOf("day")
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 16:35:00 +08:00
|
|
|
|
const loadContestData = async () => {
|
|
|
|
|
|
if (!contestId.value) return
|
|
|
|
|
|
pageLoading.value = true
|
2026-01-08 09:17:46 +08:00
|
|
|
|
try {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
const c = await contestsApi.getDetail(contestId.value)
|
|
|
|
|
|
form.contestName = c.contestName || ""
|
|
|
|
|
|
form.contestType = c.contestType || "individual"
|
|
|
|
|
|
form.startTime = c.startTime || ""
|
|
|
|
|
|
form.endTime = c.endTime || ""
|
|
|
|
|
|
form.content = c.content || ""
|
|
|
|
|
|
form.coverUrl = c.coverUrl || ""
|
|
|
|
|
|
form.posterUrl = c.posterUrl || ""
|
|
|
|
|
|
form.organizers = Array.isArray(c.organizers) ? c.organizers.join("、") : c.organizers || ""
|
|
|
|
|
|
form.coOrganizers = Array.isArray(c.coOrganizers) ? c.coOrganizers.join("、") : c.coOrganizers || ""
|
|
|
|
|
|
form.sponsors = Array.isArray(c.sponsors) ? c.sponsors.join("、") : c.sponsors || ""
|
|
|
|
|
|
form.registerStartTime = c.registerStartTime || ""
|
|
|
|
|
|
form.registerEndTime = c.registerEndTime || ""
|
|
|
|
|
|
form.submitRule = c.submitRule || "once"
|
|
|
|
|
|
form.submitStartTime = c.submitStartTime || ""
|
|
|
|
|
|
form.submitEndTime = c.submitEndTime || ""
|
|
|
|
|
|
form.reviewRuleId = c.reviewRuleId || undefined
|
|
|
|
|
|
form.reviewStartTime = c.reviewStartTime || ""
|
|
|
|
|
|
form.reviewEndTime = c.reviewEndTime || ""
|
|
|
|
|
|
form.resultPublishTime = c.resultPublishTime || ""
|
|
|
|
|
|
|
|
|
|
|
|
if (c.startTime && c.endTime) timeRange.value = [dayjs(c.startTime), dayjs(c.endTime)]
|
|
|
|
|
|
if (c.registerStartTime && c.registerEndTime) registerTimeRange.value = [dayjs(c.registerStartTime), dayjs(c.registerEndTime)]
|
|
|
|
|
|
if (c.submitStartTime && c.submitEndTime) submitTimeRange.value = [dayjs(c.submitStartTime), dayjs(c.submitEndTime)]
|
|
|
|
|
|
if (c.reviewStartTime && c.reviewEndTime) reviewTimeRange.value = [dayjs(c.reviewStartTime), dayjs(c.reviewEndTime)]
|
|
|
|
|
|
if (c.resultPublishTime) resultPublishTime.value = dayjs(c.resultPublishTime)
|
|
|
|
|
|
|
|
|
|
|
|
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.attachments?.length) {
|
|
|
|
|
|
attachmentFileList.value = c.attachments.map((att: any, i: number) => ({ uid: `-${i + 3}`, name: att.fileName, status: "done", url: att.fileUrl }))
|
2026-01-15 16:35:00 +08:00
|
|
|
|
}
|
2026-03-31 20:02:24 +08:00
|
|
|
|
} catch (e: any) { message.error(e?.response?.data?.message || "加载活动数据失败"); router.back() }
|
|
|
|
|
|
finally { 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
|
|
|
|
const submitData: CreateContestForm = {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
contestName: form.contestName, contestType: form.contestType, 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, submitStartTime: form.submitStartTime, submitEndTime: form.submitEndTime,
|
2026-01-15 16:35:00 +08:00
|
|
|
|
reviewRuleId: form.reviewRuleId || undefined,
|
2026-03-31 20:02:24 +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) {
|
|
|
|
|
|
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)
|
|
|
|
|
|
if (attachmentFileList.value.length > 0) {
|
|
|
|
|
|
try {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
await Promise.all(attachmentFileList.value.map(file => {
|
|
|
|
|
|
const fileUrl = file.url || file.response?.url || file.response?.data?.url
|
2026-01-15 16:35:00 +08:00
|
|
|
|
if (fileUrl && file.name) {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
return attachmentsApi.create({ contestId: contest.id, fileName: file.name, fileUrl, format: file.name.split(".").pop()?.toLowerCase(), size: file.size?.toString() })
|
2026-01-15 16:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
return Promise.resolve()
|
2026-03-31 20:02:24 +08:00
|
|
|
|
}))
|
|
|
|
|
|
} catch { message.warning("活动创建成功,但部分附件记录创建失败") }
|
2026-01-15 16:35:00 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
message.success("创建成功")
|
|
|
|
|
|
}
|
2026-04-03 13:49:19 +08:00
|
|
|
|
router.push(`/${tenantCode}/contests/list`)
|
2026-03-31 20:02:24 +08:00
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
if (e?.errorFields) return
|
|
|
|
|
|
message.error(e?.response?.data?.message || (isEdit.value ? "保存失败" : "创建失败"))
|
|
|
|
|
|
} finally { submitLoading.value = false }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 20:02:24 +08:00
|
|
|
|
const handleCancel = () => { router.back() }
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-15 16:35:00 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchReviewRules()
|
2026-03-31 20:02:24 +08:00
|
|
|
|
if (isEdit.value) loadContestData()
|
2026-01-08 09:17:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-03-31 20:02:24 +08:00
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
$primary: #6366f1;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-03-31 20:02:24 +08:00
|
|
|
|
.create-contest-page {
|
|
|
|
|
|
max-width: 960px;
|
|
|
|
|
|
margin: 0 auto;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 20:02:24 +08:00
|
|
|
|
.page-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 20:02:24 +08:00
|
|
|
|
.contest-form {
|
|
|
|
|
|
:deep(.ant-form-item-label > label) {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 20:02:24 +08:00
|
|
|
|
.form-section {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1e1b4b;
|
|
|
|
|
|
margin: 0 0 20px;
|
|
|
|
|
|
padding-bottom: 12px;
|
|
|
|
|
|
border-bottom: 1px solid #f0ecf9;
|
|
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 20:02:24 +08:00
|
|
|
|
.form-hint {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
margin-top: 4px;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-actions {
|
2026-03-31 20:02:24 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|