|
|
|
|
@ -231,11 +231,17 @@ const applicationStatus = ref<'draft' | 'submitted'>('draft')
|
|
|
|
|
const submittedAt = ref('')
|
|
|
|
|
/** 与后端 participant_may_edit 对齐:有评审记录后为 false,表单整表禁用 */
|
|
|
|
|
const participantMayEdit = ref(true)
|
|
|
|
|
/** 与后端 participant_may_edit_files 对齐:截止后白名单可仅改附件 */
|
|
|
|
|
const participantMayEditFiles = 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 && participantMayEditFiles.value && !participantMayEdit.value) {
|
|
|
|
|
return '报名已提交,仅可补传附件'
|
|
|
|
|
}
|
|
|
|
|
if (isSubmitted.value) return '报名已提交,已锁定'
|
|
|
|
|
if (!participantMayEdit.value && participantMayEditFiles.value) return '报名未提交,仅可补传附件'
|
|
|
|
|
if (!participantMayEdit.value) return '报名未提交,当前不可编辑'
|
|
|
|
|
return '报名信息未提交'
|
|
|
|
|
})
|
|
|
|
|
@ -244,6 +250,10 @@ const applicationStatusBody = computed(() => {
|
|
|
|
|
const time = submittedAtText.value ? `上次提交时间:${submittedAtText.value}。` : ''
|
|
|
|
|
return `${time}在报名截止且没有任一评委提交评分前,你仍可修改资料、增删附件,并点击“更新报名”覆盖提交。评审开始后报名会自动锁定,请保持联系方式畅通。`
|
|
|
|
|
}
|
|
|
|
|
if (participantMayEditFiles.value && !participantMayEdit.value) {
|
|
|
|
|
const time = submittedAtText.value ? `提交时间:${submittedAtText.value}。` : ''
|
|
|
|
|
return `${time}报名已截止,组委会已开放你补传附件:可上传或删除附件,上传成功即生效,无需再次提交。其他报名信息不可修改;若已有评委评审则仍会锁定。`
|
|
|
|
|
}
|
|
|
|
|
if (isSubmitted.value) {
|
|
|
|
|
const time = submittedAtText.value ? `提交时间:${submittedAtText.value}。` : ''
|
|
|
|
|
return `${time}报名已因截止或评审记录产生而锁定,不能再修改资料、增删附件或重复提交。后续请等待赛事组委会通知;如需更正请联系组委会。`
|
|
|
|
|
@ -343,6 +353,8 @@ const supportingFilesFeedback = ref('')
|
|
|
|
|
|
|
|
|
|
const wasValidated = ref(false)
|
|
|
|
|
const formDisabled = computed(() => !participantMayEdit.value)
|
|
|
|
|
/** 附件上传/删除:截止后白名单可单独放开 */
|
|
|
|
|
const fileControlsDisabled = computed(() => !participantMayEditFiles.value)
|
|
|
|
|
|
|
|
|
|
const applyFormEl = ref<HTMLFormElement | null>(null)
|
|
|
|
|
/** 在 v-for 中绑定时 Vue 可能将 ref 设为元素数组 */
|
|
|
|
|
@ -945,20 +957,35 @@ async function openFilePreview(item: FileItem) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(url, { credentials: 'omit' })
|
|
|
|
|
if (!r.ok) throw new Error('fetch failed')
|
|
|
|
|
const r = await fetch(url, {
|
|
|
|
|
credentials: 'omit',
|
|
|
|
|
headers: { Accept: 'application/json,*/*' },
|
|
|
|
|
})
|
|
|
|
|
if (!r.ok) {
|
|
|
|
|
let message = '附件文件不存在或已丢失'
|
|
|
|
|
try {
|
|
|
|
|
const payload = (await r.json()) as { message?: string }
|
|
|
|
|
if (typeof payload.message === 'string' && payload.message.trim()) {
|
|
|
|
|
message = payload.message.trim()
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore non-json error bodies
|
|
|
|
|
}
|
|
|
|
|
await showNotice(message, '提示', 'warning')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const blob = await r.blob()
|
|
|
|
|
const blobUrl = URL.createObjectURL(blob)
|
|
|
|
|
item.localBlobUrl = blobUrl
|
|
|
|
|
window.open(blobUrl, '_blank', 'noopener,noreferrer')
|
|
|
|
|
} catch {
|
|
|
|
|
window.open(url, '_blank', 'noopener,noreferrer')
|
|
|
|
|
await showNotice('网络错误,无法打开附件', '提示', 'warning')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeFile(id: string | number, target: 'plan' | 'supporting') {
|
|
|
|
|
const key = String(id)
|
|
|
|
|
if (deletingFileIds.value.includes(key)) return
|
|
|
|
|
if (fileControlsDisabled.value || deletingFileIds.value.includes(key)) return
|
|
|
|
|
|
|
|
|
|
let items = target === 'plan' ? [...planFileItems.value] : [...supportingFileItems.value]
|
|
|
|
|
const item = items.find((e) => String(e.id) === key)
|
|
|
|
|
@ -1014,7 +1041,7 @@ async function removeFile(id: string | number, target: 'plan' | 'supporting') {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addFiles(fileList: FileList | null, target: 'plan' | 'supporting') {
|
|
|
|
|
if (!fileList?.length) return
|
|
|
|
|
if (!fileList?.length || fileControlsDisabled.value) return
|
|
|
|
|
const field = fileFieldSchema(target)
|
|
|
|
|
const maxC = effectiveMaxFileCountForField(field)
|
|
|
|
|
const items = target === 'plan' ? [...planFileItems.value] : [...supportingFileItems.value]
|
|
|
|
|
@ -1149,6 +1176,7 @@ async function loadPublicCompetition() {
|
|
|
|
|
function applyServerPayload(d: {
|
|
|
|
|
status?: string
|
|
|
|
|
participant_may_edit?: boolean
|
|
|
|
|
participant_may_edit_files?: boolean
|
|
|
|
|
player_name?: string
|
|
|
|
|
school?: string
|
|
|
|
|
degree?: string
|
|
|
|
|
@ -1172,6 +1200,10 @@ function applyServerPayload(d: {
|
|
|
|
|
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
|
|
|
|
|
participantMayEditFiles.value =
|
|
|
|
|
typeof d.participant_may_edit_files === 'boolean'
|
|
|
|
|
? d.participant_may_edit_files
|
|
|
|
|
: d.participant_may_edit !== false
|
|
|
|
|
for (const f of schemaFields.value) {
|
|
|
|
|
if (f.type === 'file') continue
|
|
|
|
|
if (f.key === 'commitment_accepted' || f.key === 'promise_signature') continue
|
|
|
|
|
@ -1573,7 +1605,7 @@ onMounted(() => {
|
|
|
|
|
:class="{ 'is-invalid': wasValidated && !!planFileFeedback }"
|
|
|
|
|
:accept="fileInputAcceptAttr(field)"
|
|
|
|
|
multiple
|
|
|
|
|
:disabled="formDisabled"
|
|
|
|
|
:disabled="fileControlsDisabled"
|
|
|
|
|
@change="addFiles(($event.target as HTMLInputElement).files, 'plan')"
|
|
|
|
|
/>
|
|
|
|
|
<div class="apply-file-status" :class="{ 'apply-file-status--filled': planFileItems.length > 0 }">
|
|
|
|
|
@ -1609,7 +1641,7 @@ onMounted(() => {
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-sm btn-outline-danger"
|
|
|
|
|
:disabled="formDisabled || deletingFileIds.includes(String(item.id))"
|
|
|
|
|
:disabled="fileControlsDisabled || deletingFileIds.includes(String(item.id))"
|
|
|
|
|
@click="removeFile(item.id, 'plan')"
|
|
|
|
|
>
|
|
|
|
|
{{ deletingFileIds.includes(String(item.id)) ? '删除中…' : '删除' }}
|
|
|
|
|
@ -1638,7 +1670,7 @@ onMounted(() => {
|
|
|
|
|
:class="{ 'is-invalid': wasValidated && !!supportingFilesFeedback }"
|
|
|
|
|
:accept="fileInputAcceptAttr(field)"
|
|
|
|
|
multiple
|
|
|
|
|
:disabled="formDisabled"
|
|
|
|
|
:disabled="fileControlsDisabled"
|
|
|
|
|
@change="addFiles(($event.target as HTMLInputElement).files, 'supporting')"
|
|
|
|
|
/>
|
|
|
|
|
<div class="apply-file-status" :class="{ 'apply-file-status--filled': supportingFileItems.length > 0 }">
|
|
|
|
|
@ -1674,7 +1706,7 @@ onMounted(() => {
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-sm btn-outline-danger"
|
|
|
|
|
:disabled="formDisabled || deletingFileIds.includes(String(item.id))"
|
|
|
|
|
:disabled="fileControlsDisabled || deletingFileIds.includes(String(item.id))"
|
|
|
|
|
@click="removeFile(item.id, 'supporting')"
|
|
|
|
|
>
|
|
|
|
|
{{ deletingFileIds.includes(String(item.id)) ? '删除中…' : '删除' }}
|
|
|
|
|
|