|
|
|
|
@ -22,14 +22,17 @@ import {
|
|
|
|
|
participantApplyPageTitle,
|
|
|
|
|
type BrandingForm,
|
|
|
|
|
} from '../utils/competitionBranding'
|
|
|
|
|
import { normalizeSignupSchema, type SignupFormSchemaField } from '../utils/defaultSignupFormSchema'
|
|
|
|
|
import {
|
|
|
|
|
CONTEST_DEFAULT_FILE_EXTENSIONS,
|
|
|
|
|
normalizeSignupSchema,
|
|
|
|
|
type SignupFormSchemaField,
|
|
|
|
|
} from '../utils/defaultSignupFormSchema'
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
|
|
|
|
const slug = computed(() => String(route.params.slug ?? '').trim())
|
|
|
|
|
|
|
|
|
|
const allowedExt = ['pdf', 'ppt', 'pptx', 'doc', 'docx', 'wps', 'rar', 'zip']
|
|
|
|
|
const maxFileSize = 20 * 1024 * 1024
|
|
|
|
|
const MOBILE_PATTERN = '^1[3-9]\\d{9}$'
|
|
|
|
|
|
|
|
|
|
@ -104,6 +107,46 @@ function goLogin() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const schemaFields = ref<SignupFormSchemaField[]>(normalizeSignupSchema([]))
|
|
|
|
|
|
|
|
|
|
/** schema 文件字段允许的扩展名(与 config/contest.file_mimes ∩ schema.file_extensions) */
|
|
|
|
|
function effectiveFileExtensionsForField(field: SignupFormSchemaField | undefined): string[] {
|
|
|
|
|
const globe = CONTEST_DEFAULT_FILE_EXTENSIONS.map((e) => e.toLowerCase())
|
|
|
|
|
const globSet = new Set(globe)
|
|
|
|
|
const raw = field?.file_extensions
|
|
|
|
|
if (!Array.isArray(raw) || raw.length === 0) return [...globe]
|
|
|
|
|
const cleaned = [
|
|
|
|
|
...new Set(
|
|
|
|
|
raw
|
|
|
|
|
.map((x) =>
|
|
|
|
|
String(x)
|
|
|
|
|
.trim()
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/^\./, ''),
|
|
|
|
|
)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.filter((e) => globSet.has(e)),
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
return cleaned.length > 0 ? cleaned : [...globe]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** schema 配置了 file_max_count 时返回上限,否则不限 */
|
|
|
|
|
function effectiveMaxFileCountForField(field: SignupFormSchemaField | undefined): number | null {
|
|
|
|
|
const n = field?.file_max_count
|
|
|
|
|
if (typeof n !== 'number' || !Number.isFinite(n)) return null
|
|
|
|
|
const i = Math.floor(n)
|
|
|
|
|
return i >= 1 ? i : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fileFieldSchema(target: 'plan' | 'supporting'): SignupFormSchemaField | undefined {
|
|
|
|
|
return schemaFields.value.find((f) => f.type === 'file' && f.key === target)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fileInputAcceptAttr(field: SignupFormSchemaField | undefined): string {
|
|
|
|
|
return effectiveFileExtensionsForField(field)
|
|
|
|
|
.map((e) => '.' + e)
|
|
|
|
|
.join(',')
|
|
|
|
|
}
|
|
|
|
|
const competitionTracks = ref<PublicTrackRow[]>([])
|
|
|
|
|
/** 赛事 /public API 已成功解析后为 true,避免 tracks 尚未返回时出现黄色提示闪烁 */
|
|
|
|
|
const competitionHydrated = ref(false)
|
|
|
|
|
@ -569,20 +612,29 @@ function validateFileItems(
|
|
|
|
|
items: FileItem[],
|
|
|
|
|
missingMessage: string,
|
|
|
|
|
optional: boolean,
|
|
|
|
|
field?: SignupFormSchemaField,
|
|
|
|
|
): { ok: boolean; feedback: string } {
|
|
|
|
|
const allowed = effectiveFileExtensionsForField(field)
|
|
|
|
|
const maxCount = effectiveMaxFileCountForField(field)
|
|
|
|
|
const realFiles = items.filter((i) => i.file)
|
|
|
|
|
const serverOnly = items.filter((i) => i.fromServer && !i.file)
|
|
|
|
|
if (!realFiles.length && !serverOnly.length) {
|
|
|
|
|
return { ok: optional, feedback: optional ? '' : missingMessage }
|
|
|
|
|
}
|
|
|
|
|
if (maxCount !== null && items.length > maxCount) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
feedback: `最多可上传 ${maxCount} 个文件,请删除多余文件后重试`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const invalidExtFile = realFiles.map((i) => i.file!).find((file) => {
|
|
|
|
|
const ext = (file.name.split('.').pop() || '').toLowerCase()
|
|
|
|
|
return !allowedExt.includes(ext)
|
|
|
|
|
return !allowed.includes(ext)
|
|
|
|
|
})
|
|
|
|
|
if (invalidExtFile) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
feedback: `“${invalidExtFile.name}”格式不支持,请上传 PDF/PPT/PPTX/DOC/DOCX/WPS/RAR/ZIP`,
|
|
|
|
|
feedback: `“${invalidExtFile.name}”格式不支持,请上传 ${allowed.join('、')} 格式的文件`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const oversizedFile = realFiles.map((i) => i.file!).find((file) => file.size > maxFileSize)
|
|
|
|
|
@ -605,7 +657,8 @@ function validatePlanFile() {
|
|
|
|
|
templateRefEl(planFileInput)?.setCustomValidity('')
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
const r = validateFileItems(planFileItems.value, '请上传商业计划书', false)
|
|
|
|
|
const planField = fileFieldSchema('plan')
|
|
|
|
|
const r = validateFileItems(planFileItems.value, '请上传商业计划书', false, planField)
|
|
|
|
|
planFileFeedback.value = r.feedback
|
|
|
|
|
templateRefEl(planFileInput)?.setCustomValidity(r.ok ? '' : 'missing')
|
|
|
|
|
return r.ok
|
|
|
|
|
@ -616,7 +669,8 @@ function validateSupportingFiles() {
|
|
|
|
|
templateRefEl(supportingFileInput)?.setCustomValidity('')
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
const r = validateFileItems(supportingFileItems.value, '文件格式或大小不符合要求', true)
|
|
|
|
|
const supField = fileFieldSchema('supporting')
|
|
|
|
|
const r = validateFileItems(supportingFileItems.value, '文件格式或大小不符合要求', true, supField)
|
|
|
|
|
supportingFilesFeedback.value = r.feedback
|
|
|
|
|
templateRefEl(supportingFileInput)?.setCustomValidity(r.ok ? '' : 'bad')
|
|
|
|
|
return r.ok
|
|
|
|
|
@ -733,8 +787,20 @@ async function removeFile(id: string | number, target: 'plan' | 'supporting') {
|
|
|
|
|
|
|
|
|
|
function addFiles(fileList: FileList | null, target: 'plan' | 'supporting') {
|
|
|
|
|
if (!fileList?.length) return
|
|
|
|
|
const items = target === 'plan' ? planFileItems.value : supportingFileItems.value
|
|
|
|
|
Array.from(fileList).forEach((file) => {
|
|
|
|
|
const field = fileFieldSchema(target)
|
|
|
|
|
const maxC = effectiveMaxFileCountForField(field)
|
|
|
|
|
const items = target === 'plan' ? [...planFileItems.value] : [...supportingFileItems.value]
|
|
|
|
|
let incoming = Array.from(fileList)
|
|
|
|
|
const room = maxC === null ? incoming.length : Math.max(0, maxC - items.length)
|
|
|
|
|
if (incoming.length > room && maxC !== null) {
|
|
|
|
|
showNotice(
|
|
|
|
|
`该栏最多上传 ${maxC} 个文件,已忽略多余 ${incoming.length - room} 个选择。`,
|
|
|
|
|
'提示',
|
|
|
|
|
'warning',
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
incoming = incoming.slice(0, room)
|
|
|
|
|
incoming.forEach((file) => {
|
|
|
|
|
items.push({
|
|
|
|
|
id: `tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
|
|
|
file,
|
|
|
|
|
@ -1179,7 +1245,7 @@ onMounted(() => {
|
|
|
|
|
type="file"
|
|
|
|
|
class="form-control editable"
|
|
|
|
|
:class="{ 'is-invalid': wasValidated && !!planFileFeedback }"
|
|
|
|
|
accept=".pdf,.ppt,.pptx,.doc,.docx,.wps,.rar,.zip"
|
|
|
|
|
:accept="fileInputAcceptAttr(field)"
|
|
|
|
|
multiple
|
|
|
|
|
:disabled="formDisabled"
|
|
|
|
|
@change="addFiles(($event.target as HTMLInputElement).files, 'plan')"
|
|
|
|
|
@ -1242,7 +1308,7 @@ onMounted(() => {
|
|
|
|
|
type="file"
|
|
|
|
|
class="form-control editable"
|
|
|
|
|
:class="{ 'is-invalid': wasValidated && !!supportingFilesFeedback }"
|
|
|
|
|
accept=".pdf,.ppt,.pptx,.doc,.docx,.wps,.rar,.zip"
|
|
|
|
|
:accept="fileInputAcceptAttr(field)"
|
|
|
|
|
multiple
|
|
|
|
|
:disabled="formDisabled"
|
|
|
|
|
@change="addFiles(($event.target as HTMLInputElement).files, 'supporting')"
|
|
|
|
|
|