|
|
|
|
@ -211,8 +211,31 @@ function ensureFormKeys(fields: SignupFormSchemaField[]) {
|
|
|
|
|
const STORAGE_KEY = computed(() => `jscc_signup_form_draft_v2_${slug.value || 'x'}`)
|
|
|
|
|
|
|
|
|
|
const applicationStatus = ref<'draft' | 'submitted'>('draft')
|
|
|
|
|
const submittedAt = ref('')
|
|
|
|
|
/** 与后端 participant_may_edit 对齐:有评审记录后为 false,表单整表禁用 */
|
|
|
|
|
const participantMayEdit = ref(true)
|
|
|
|
|
const isSubmitted = computed(() => applicationStatus.value === 'submitted')
|
|
|
|
|
const submittedAtText = computed(() => formatSubmittedAt(submittedAt.value))
|
|
|
|
|
const applicationStatusTitle = computed(() => {
|
|
|
|
|
if (isSubmitted.value && participantMayEdit.value) return '报名已提交,可继续修改'
|
|
|
|
|
if (isSubmitted.value) return '报名已提交,已锁定'
|
|
|
|
|
if (!participantMayEdit.value) return '报名未提交,当前不可编辑'
|
|
|
|
|
return '报名信息未提交'
|
|
|
|
|
})
|
|
|
|
|
const applicationStatusBody = computed(() => {
|
|
|
|
|
if (isSubmitted.value && participantMayEdit.value) {
|
|
|
|
|
const time = submittedAtText.value ? `上次提交时间:${submittedAtText.value}。` : ''
|
|
|
|
|
return `${time}在报名截止且没有任一评委提交评分前,你仍可修改资料、增删附件,并点击“更新报名”覆盖提交。评审开始后报名会自动锁定,请保持联系方式畅通。`
|
|
|
|
|
}
|
|
|
|
|
if (isSubmitted.value) {
|
|
|
|
|
const time = submittedAtText.value ? `提交时间:${submittedAtText.value}。` : ''
|
|
|
|
|
return `${time}报名已因截止或评审记录产生而锁定,不能再修改资料、增删附件或重复提交。后续请等待赛事组委会通知;如需更正请联系组委会。`
|
|
|
|
|
}
|
|
|
|
|
if (!participantMayEdit.value) {
|
|
|
|
|
return '当前报名窗口已关闭或报名已被锁定,无法新建、保存或提交报名。如认为状态异常,请联系赛事组委会。'
|
|
|
|
|
}
|
|
|
|
|
return '请完整填写报名信息、上传商业计划书并签署承诺书;点击“保存”只暂存草稿,点击“提交报名”后才进入已提交状态。'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const trackInvalid = ref(false)
|
|
|
|
|
const trackHiddenSelect = ref<HTMLSelectElement | HTMLSelectElement[] | null>(null)
|
|
|
|
|
@ -406,6 +429,25 @@ function fileItemSuggestedDownloadName(item: FileItem): string | undefined {
|
|
|
|
|
return n || undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatSubmittedAt(value: string): string {
|
|
|
|
|
if (!value) return ''
|
|
|
|
|
const d = new Date(value)
|
|
|
|
|
if (Number.isNaN(d.getTime())) return ''
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function uploadedFileCountText(items: FileItem[]): string {
|
|
|
|
|
if (items.length === 0) return '尚未选择文件'
|
|
|
|
|
const uploaded = items.filter((item) => item.fromServer).length
|
|
|
|
|
if (uploaded === items.length) return `已上传 ${items.length} 个文件`
|
|
|
|
|
return `已选择 ${items.length} 个文件,正在上传 ${items.length - uploaded} 个`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectPlaceholder(field: SignupFormSchemaField): string {
|
|
|
|
|
return `请选择${field.label}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatApiErrSafe(data: Record<string, unknown>) {
|
|
|
|
|
const msg = (data.message as string) || ''
|
|
|
|
|
const errObj = data.errors as Record<string, string[]> | undefined
|
|
|
|
|
@ -1051,9 +1093,11 @@ function applyServerPayload(d: {
|
|
|
|
|
intro?: string
|
|
|
|
|
promise_signed_at?: string | null
|
|
|
|
|
promise_signature?: string | null
|
|
|
|
|
submitted_at?: string | null
|
|
|
|
|
files?: { id: number; kind: string; original_name: string; size: number; url: string }[]
|
|
|
|
|
}) {
|
|
|
|
|
applicationStatus.value = (d.status as 'draft' | 'submitted') || 'draft'
|
|
|
|
|
submittedAt.value = typeof d.submitted_at === 'string' ? d.submitted_at : ''
|
|
|
|
|
participantMayEdit.value = d.participant_may_edit !== false
|
|
|
|
|
formModel.player_name = d.player_name || ''
|
|
|
|
|
formModel.school = d.school || ''
|
|
|
|
|
@ -1294,6 +1338,28 @@ onMounted(() => {
|
|
|
|
|
<div v-if="!loadError" class="apply-form-scroll">
|
|
|
|
|
<div class="card mb-0">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div
|
|
|
|
|
class="application-status-panel"
|
|
|
|
|
:class="isSubmitted ? 'application-status-panel--submitted' : 'application-status-panel--draft'"
|
|
|
|
|
role="status"
|
|
|
|
|
aria-live="polite"
|
|
|
|
|
>
|
|
|
|
|
<div class="application-status-mark" aria-hidden="true">
|
|
|
|
|
<svg v-if="isSubmitted" viewBox="0 0 24 24" width="22" height="22" fill="none">
|
|
|
|
|
<circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="1.8" />
|
|
|
|
|
<path d="M7.6 12.2l2.8 2.8 6-6.2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
</svg>
|
|
|
|
|
<svg v-else viewBox="0 0 24 24" width="22" height="22" fill="none">
|
|
|
|
|
<circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="1.8" />
|
|
|
|
|
<path d="M12 7.5v5.2" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
|
|
|
|
<circle cx="12" cy="16.5" r="1" fill="currentColor" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="application-status-copy">
|
|
|
|
|
<strong>{{ applicationStatusTitle }}</strong>
|
|
|
|
|
<span>{{ applicationStatusBody }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<form
|
|
|
|
|
id="applyForm"
|
|
|
|
|
ref="applyFormEl"
|
|
|
|
|
@ -1342,9 +1408,9 @@ onMounted(() => {
|
|
|
|
|
>
|
|
|
|
|
<span class="track-toggle-label text-truncate">
|
|
|
|
|
<span :class="trackMainClass">{{ trackMainText }}</span>
|
|
|
|
|
<span v-if="trackNoteText" class="track-custom-note">{{ trackNoteText }}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
<small v-if="trackNoteText" class="track-selected-note">{{ trackNoteText }}</small>
|
|
|
|
|
<ul class="dropdown-menu w-100 border shadow-sm rounded-2 py-0 my-1 track-custom-menu">
|
|
|
|
|
<li v-for="t in competitionTracks" :key="t.track_code">
|
|
|
|
|
<button
|
|
|
|
|
@ -1391,6 +1457,9 @@ onMounted(() => {
|
|
|
|
|
:disabled="formDisabled"
|
|
|
|
|
@change="addFiles(($event.target as HTMLInputElement).files, 'plan')"
|
|
|
|
|
/>
|
|
|
|
|
<div class="apply-file-status" :class="{ 'apply-file-status--filled': planFileItems.length > 0 }">
|
|
|
|
|
{{ uploadedFileCountText(planFileItems) }}
|
|
|
|
|
</div>
|
|
|
|
|
<template v-if="participantFieldHelp(field.help)">
|
|
|
|
|
<small class="text-secondary prototype-subtitle d-block mt-1">{{
|
|
|
|
|
participantFieldHelp(field.help)
|
|
|
|
|
@ -1417,7 +1486,7 @@ onMounted(() => {
|
|
|
|
|
:download="fileItemSuggestedDownloadName(item)"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener"
|
|
|
|
|
>预览</a
|
|
|
|
|
>查看/下载</a
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
@ -1454,6 +1523,9 @@ onMounted(() => {
|
|
|
|
|
:disabled="formDisabled"
|
|
|
|
|
@change="addFiles(($event.target as HTMLInputElement).files, 'supporting')"
|
|
|
|
|
/>
|
|
|
|
|
<div class="apply-file-status" :class="{ 'apply-file-status--filled': supportingFileItems.length > 0 }">
|
|
|
|
|
{{ uploadedFileCountText(supportingFileItems) }}
|
|
|
|
|
</div>
|
|
|
|
|
<template v-if="participantFieldHelp(field.help)">
|
|
|
|
|
<small class="text-secondary prototype-subtitle d-block mt-1">{{
|
|
|
|
|
participantFieldHelp(field.help)
|
|
|
|
|
@ -1480,7 +1552,7 @@ onMounted(() => {
|
|
|
|
|
:download="fileItemSuggestedDownloadName(item)"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener"
|
|
|
|
|
>预览</a
|
|
|
|
|
>查看/下载</a
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
@ -1515,6 +1587,7 @@ onMounted(() => {
|
|
|
|
|
:required="effectiveRequired(field)"
|
|
|
|
|
:disabled="formDisabled"
|
|
|
|
|
>
|
|
|
|
|
<option value="" disabled>{{ selectPlaceholder(field) }}</option>
|
|
|
|
|
<option v-for="opt in optionsForField(field)" :key="`${field.key}-${opt.value}`" :value="opt.value">
|
|
|
|
|
{{ opt.label }}
|
|
|
|
|
</option>
|
|
|
|
|
@ -1543,6 +1616,9 @@ onMounted(() => {
|
|
|
|
|
:required="effectiveRequired(field)"
|
|
|
|
|
:disabled="formDisabled || !isChina || !String(formModel.location_province ?? '').trim()"
|
|
|
|
|
>
|
|
|
|
|
<option value="" disabled>
|
|
|
|
|
{{ String(formModel.location_province ?? '').trim() ? selectPlaceholder(field) : '请先选择省份' }}
|
|
|
|
|
</option>
|
|
|
|
|
<option v-for="opt in optionsForField(field)" :key="opt.value" :value="opt.value">
|
|
|
|
|
{{ opt.label }}
|
|
|
|
|
</option>
|
|
|
|
|
@ -1696,8 +1772,12 @@ onMounted(() => {
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div class="col-12 apply-form-actions mt-3 pt-3 border-top d-flex gap-2 flex-wrap">
|
|
|
|
|
<button type="button" class="btn btn-outline-primary" :disabled="formDisabled" @click="onSaveClick">保存</button>
|
|
|
|
|
<button type="button" class="btn btn-success" :disabled="formDisabled" @click="onSubmitClick">提交报名</button>
|
|
|
|
|
<button type="button" class="btn btn-outline-primary" :disabled="formDisabled" @click="onSaveClick">
|
|
|
|
|
{{ isSubmitted ? '保存修改' : '保存' }}
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" class="btn btn-success" :disabled="formDisabled" @click="onSubmitClick">
|
|
|
|
|
{{ isSubmitted ? '更新报名' : '提交报名' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
@ -1814,6 +1894,141 @@ onMounted(() => {
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.application-status-panel {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
padding: 0.875rem 1rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
border: 1px solid #d8e2ef;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: #f7f9fc;
|
|
|
|
|
color: #17324d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.application-status-panel--submitted {
|
|
|
|
|
border-color: #bdd7c8;
|
|
|
|
|
background: #f3faf5;
|
|
|
|
|
color: #1f5132;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.application-status-mark {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 1.75rem;
|
|
|
|
|
height: 1.75rem;
|
|
|
|
|
margin-top: 0.05rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.application-status-copy {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 0.2rem;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.application-status-copy strong {
|
|
|
|
|
font-size: 0.98rem;
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.application-status-copy span {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
color: #4d5f73;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.application-status-panel--submitted .application-status-copy span {
|
|
|
|
|
color: #42664f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-selected-note {
|
|
|
|
|
display: block;
|
|
|
|
|
margin-top: 0.35rem;
|
|
|
|
|
color: #6c7886;
|
|
|
|
|
font-size: 0.84rem;
|
|
|
|
|
line-height: 1.45;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-pick-inline {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(5.5rem, auto) 1fr;
|
|
|
|
|
gap: 0.6rem;
|
|
|
|
|
align-items: start;
|
|
|
|
|
white-space: normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-pick-inline .text-body {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #1f1f1f !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-custom-note {
|
|
|
|
|
color: #7d8896;
|
|
|
|
|
font-size: 0.86rem;
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.apply-file-status {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
min-height: 1.75rem;
|
|
|
|
|
margin-top: 0.45rem;
|
|
|
|
|
padding: 0.25rem 0.6rem;
|
|
|
|
|
border: 1px solid #e4e9ef;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: #f7f9fb;
|
|
|
|
|
color: #667585;
|
|
|
|
|
font-size: 0.84rem;
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.apply-file-status--filled {
|
|
|
|
|
border-color: #bdd7c8;
|
|
|
|
|
background: #f3faf5;
|
|
|
|
|
color: #1f5132;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 575.98px) {
|
|
|
|
|
.application-status-panel {
|
|
|
|
|
padding: 0.8rem 0.85rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-custom-menu {
|
|
|
|
|
max-height: min(62vh, 28rem);
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
overscroll-behavior: contain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-pick {
|
|
|
|
|
padding: 0.85rem 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-pick-inline {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-custom-note {
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.track-selected-note {
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.apply-file-status {
|
|
|
|
|
display: flex;
|
|
|
|
|
width: 100%;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.promise-doc-body--rich {
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
|