weizong song 4 months ago
parent d813605272
commit dfda6c599c

@ -261,9 +261,21 @@ body.prototype-page #applyForm select.form-select option {
}
body.prototype-page #applyForm input[type='file'].form-control {
min-height: auto !important;
padding-top: 0.38rem !important;
padding-bottom: 0.38rem !important;
min-height: 2.75rem !important;
padding: 0.42rem !important;
color: #4d5f73 !important;
cursor: pointer;
}
body.prototype-page #applyForm input[type='file'].form-control::file-selector-button {
min-height: 1.85rem;
margin-right: 0.7rem;
border: 0;
border-radius: 6px;
background: #052d62;
color: #fff;
font-weight: 600;
padding: 0.32rem 0.85rem;
}
body.prototype-page #applyForm .track-custom-toggle.form-select {
@ -298,6 +310,32 @@ body.prototype-page #applyForm .track-custom-toggle.form-select::after {
vertical-align: middle;
}
body.prototype-page #applyForm .track-custom-menu {
max-height: min(56vh, 30rem);
overflow-y: auto;
}
@media (max-width: 575.98px) {
body.prototype-page #applyForm .form-select,
body.prototype-page #applyForm .track-custom-toggle.form-select,
body.prototype-page #applyForm input.form-control:not([type='file']) {
min-height: 2.75rem !important;
font-size: 1rem !important;
}
body.prototype-page #applyForm input[type='file'].form-control {
min-height: 3rem !important;
font-size: 0.95rem !important;
}
body.prototype-page #applyForm input[type='file'].form-control::file-selector-button {
display: block;
width: 100%;
margin: 0 0 0.4rem;
min-height: 2.35rem;
}
}
body.prototype-page #applyForm.was-validated .form-control:valid,
body.prototype-page #applyForm .form-control.is-valid,
body.prototype-page #applyForm.was-validated textarea.form-control:valid,

@ -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;

Loading…
Cancel
Save