feat: 上传文件接口&表单成功提交页面

This commit is contained in:
2026-01-21 15:29:34 +08:00
parent 9bdd0a23d3
commit f8f5b98854
6 changed files with 444 additions and 191 deletions

View File

@@ -5,12 +5,19 @@
BLOOM YOUR CREATIVITY AiDA GLOBAL FASHION AWARD 2026
</div>
<div class="title poppins-medium">Application Form</div>
<div class="form-header">
<div
class="form-header"
v-if="!isCompleted"
>
<div class="form-title poppins-bold">Email Verification</div>
<div class="desc">AiDA Users Only</div>
</div>
</div>
<div class="form-container">
<Success v-if="isCompleted" />
<div
class="form-container"
v-if="!isCompleted"
>
<div class="form-content">
<a-form
name="form"
@@ -66,7 +73,22 @@
:label="item.label"
:name="item.key"
>
<a-input v-model:value="form[item.key]" />
<a-input
v-if="item.type === 'input'"
v-model:value="form[item.key]"
/>
<a-select
v-if="item.type === 'select'"
v-model:value="form[item.key]"
:options="genderOptions"
>
<template #suffixIcon>
<img
class="arrow-down-icon"
src="@/assets/images/award/arrow_down.svg"
/>
</template>
</a-select>
</a-form-item>
</template>
</div>
@@ -84,13 +106,13 @@
</a-form-item>
<a-form-item
class="full-row design-desc"
name="description"
name="designDescription"
label="Design description"
required
>
<a-textarea
class="textarea"
v-model:value="form.description"
v-model:value="form.designDescription"
placeholder="Briefly describe your design concept, inspiration, and creative direction..."
/>
</a-form-item>
@@ -307,7 +329,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onUnmounted } from 'vue'
import { ref, onUnmounted } from 'vue'
import { debounce } from 'lodash-es'
import type { Rule } from 'ant-design-vue/es/form'
import { message } from 'ant-design-vue'
@@ -315,8 +337,11 @@
import VerifycationCodeInput from './components/VerificationCodeInput.vue'
import { Https } from '@/tool/https'
import UploadStatus from './components/UploadStatus.vue'
import Success from './components/Success.vue'
const hasValidEmail = ref(true)
const isCompleted = ref(false)
const hasValidEmail = ref(false)
const formRef = ref(null)
const form = ref({
email: '',
@@ -325,14 +350,15 @@
gender: '',
occupation: '',
age: '',
counterOrRegion: '',
phone: '',
portfoilo: '',
countryRegionCity: '',
phoneNumber: '',
portfolioUrl: '',
// code: '',
designTitle: '',
description: '',
pdfPath: null,
videoPath: null
designDescription: '',
pdfPath: 'test.pdf',
videoPath: 'test.video',
secureToken: null
})
// 验证码输入组件引用
@@ -393,6 +419,20 @@
]
}
const genderOptions = [
{
label: 'Male',
value: 'Male'
},
{
label: 'Female',
value: 'Female'
},
{
label: 'Other',
value: 'Other'
}
]
const formKeys = ref([
{
label: 'First Name',
@@ -428,20 +468,20 @@
label: 'Country/Region and City',
required: true,
type: 'input',
key: 'counterOrRegion'
key: 'countryRegionCity'
},
{
label: 'Phone Number',
required: true,
type: 'input',
key: 'phone'
key: 'phoneNumber'
},
{
label: 'Portfoilo Website/Instagram(Optional)',
required: false,
type: 'input',
key: 'portfoilo'
key: 'portfolioUrl'
}
])
@@ -488,11 +528,13 @@
try {
await formRef.value.validateFields(['email'])
// TODO: 发送验证码的逻辑
message.success('Verification code sent successfully!')
await Https.axiosGet(Https.httpUrls.checkEmail, {
params: {
email: form.value.email
}
})
// 开始倒计时
startCountdown()
showModal.value = true
} catch (error) {}
}, 300)
@@ -506,6 +548,26 @@
const handleCloseModal = () => {
showModal.value = false
}
const handleVerifyCode = () => {
console.log(verifyCode.value)
if (verifyCode.value.length !== 6) {
message.error('Please enter the complete 6-digit verification code')
return
}
Https.axiosGet(Https.httpUrls.checkOTP, {
params: {
email: form.value.email,
code: verifyCode.value
},
fullData: true
}).then(res => {
form.value.secureToken = res.data.secureToken
message.success('Verification successful!')
showModal.value = false
})
}
const handleSubmitForm = () => {
const validCondition = conditionsList.value.filter(
@@ -515,24 +577,16 @@
message.error('Please check the terms and conditions')
return
}
formRef.value.validate().then(res => {
console.log(res)
}).catch(err => {
console.log(err)
message.error('Please fill in all the required fields')
})
}
const handleVerifyCode = () => {
if (verifyCode.value.length !== 6) {
message.error('Please enter the complete 6-digit verification code')
return
}
message.success('Verification successful!')
// 关闭模态框
showModal.value = false
formRef.value
.validate()
.then(res => {
console.log(res)
Https.axiosPost(Https.httpUrls.submitForm, form.value)
})
.catch(err => {
console.log(err)
message.error('Please fill in all the required fields')
})
}
const pdfList = ref([])
@@ -544,6 +598,14 @@
const pdfUploadStatus = ref<'idle' | 'uploading' | 'success' | 'error'>('idle')
const videoUploadStatus = ref<'idle' | 'uploading' | 'success' | 'error'>('idle')
const chunkUploadState: Record<
FileType,
{ uploadId: string | null; chunkSize: number; uploadedChunks: Set<number> }
> = {
pdf: { uploadId: null, chunkSize: 0, uploadedChunks: new Set() },
video: { uploadId: null, chunkSize: 0, uploadedChunks: new Set() }
}
// 统一的文件上传前验证
const beforeUploadFile = (type: FileType, file: File) => {
if (!hasValidEmail.value) {
@@ -626,6 +688,88 @@
return beforeUploadFile('video', file)
}
const initializeChunkUpload = async (type: FileType, file: File) => {
const endpoint =
type === 'pdf' ? Https.httpUrls.initPdfUpload : Https.httpUrls.initVideoUpload
const data = await Https.axiosPost(endpoint, {
fileName: file.name,
fileSize: file.size,
fileType: file.type,
email: form.value.email,
secureToken: form.value.secureToken
})
return {
uploadId: data?.uploadId as string,
chunkSize: data?.chunkSize as number
}
}
const createFileChunks = (file: File, chunkSize: number) => {
const chunks: Blob[] = []
const totalChunks = Math.ceil(file.size / chunkSize)
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
chunks.push(chunk)
}
return chunks
}
const updateUploadProgress = (
type: FileType,
uploadedCount: number,
total: number
) => {
const percent = Math.round((uploadedCount / total) * 100)
if (type === 'pdf') {
uploadProgressPdf.value = percent
} else {
uploadProgressVideo.value = percent
}
}
const uploadChunk = async (
type: FileType,
chunk: Blob,
chunkIndex: number,
totalChunks: number
) => {
const endpoint =
type === 'pdf' ? Https.httpUrls.uploadPDF : Https.httpUrls.uploadVideo
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('uploadId', chunkUploadState[type].uploadId || '')
formData.append('chunkIndex', String(chunkIndex))
formData.append('totalChunks', String(totalChunks))
formData.append('secureToken', form.value.secureToken)
await Https.axiosPost(endpoint, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
chunkUploadState[type].uploadedChunks.add(chunkIndex)
}
const completeChunkUpload = async (type: FileType, file: File) => {
const endpoint =
type === 'pdf'
? Https.httpUrls.uploadPDFComplete
: Https.httpUrls.uploadVideoComplete
await Https.axiosPost(endpoint, {
uploadId: chunkUploadState[type].uploadId,
fileName: file.name,
totalSize: file.size,
secureToken: form.value.secureToken
})
}
type FileType = 'pdf' | 'video'
const handleFileChange = (info: UploadChangeParam, type: FileType) => {
const status = info.file.status
@@ -656,65 +800,68 @@
}
// 统一的上传处理函数
const handleUploadFile = (option: any, type: FileType) => {
console.log(option, type)
const handleUploadFile = async (option: any, type: FileType) => {
const file = option.file as File
const file = option.file
if (!form.value.email) {
message.error('Please input the email address first')
option.onError?.(new Error('Email required'))
return
}
// 根据类型设置上传状态和进度
if (type === 'pdf') {
isUploadingPdf.value = true
uploadProgressPdf.value = 0
pdfUploadStatus.value = 'uploading'
} else if (type === 'video') {
} else {
isUploadingVideo.value = true
uploadProgressVideo.value = 0
videoUploadStatus.value = 'uploading'
}
const params = new FormData()
params.append('file', file)
params.append('email', form.value.email)
try {
const { uploadId, chunkSize } = await initializeChunkUpload(type, file)
chunkUploadState[type].uploadId = uploadId
chunkUploadState[type].chunkSize = chunkSize
chunkUploadState[type].uploadedChunks = new Set()
// 根据类型选择不同的上传接口
const uploadUrl = Https.httpUrls.uploadAvatar
// const uploadUrl =
// type === 'pdf' ? Https.httpUrls.uploadPDF : Https.httpUrls.uploadVideo
Https.axiosPost(uploadUrl, params, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: progressEvent => {
if (progressEvent.total) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
if (type === 'pdf') {
uploadProgressPdf.value = percentCompleted
} else if (type === 'video') {
uploadProgressVideo.value = percentCompleted
}
}
const chunks = createFileChunks(file, chunkSize)
for (let i = 0; i < chunks.length; i++) {
await uploadChunk(type, chunks[i], i, chunks.length)
updateUploadProgress(type, i + 1, chunks.length)
}
})
.then(res => {
console.log(res)
if (type === 'pdf') pdfUploadStatus.value = 'success'
if (type === 'video') videoUploadStatus.value = 'success'
option.onSuccess(res, option.file)
})
.catch(error => {
console.error('Upload error:', error)
option.onError(error)
if (type === 'pdf') {
isUploadingPdf.value = false
uploadProgressPdf.value = 0
pdfUploadStatus.value = 'error'
} else if (type === 'video') {
isUploadingVideo.value = false
uploadProgressVideo.value = 0
videoUploadStatus.value = 'error'
}
})
const res = await completeChunkUpload(type, file)
console.log('上传完成-----', res)
if (type === 'pdf') {
pdfUploadStatus.value = 'success'
uploadProgressPdf.value = 100
isUploadingPdf.value = false
form.value.pdfPath = uploadId
} else {
videoUploadStatus.value = 'success'
uploadProgressVideo.value = 100
isUploadingVideo.value = false
form.value.videoPath = uploadId
}
option.onSuccess?.({ uploadId }, option.file)
} catch (error: any) {
console.error('Upload error:', error)
message.error(error?.message || 'Upload failed')
option.onError?.(error)
if (type === 'pdf') {
isUploadingPdf.value = false
uploadProgressPdf.value = 0
pdfUploadStatus.value = 'error'
} else {
isUploadingVideo.value = false
uploadProgressVideo.value = 0
videoUploadStatus.value = 'error'
}
}
}
// PDF上传处理
@@ -773,10 +920,18 @@
.full-row {
width: 100%;
}
.arrow-down-icon {
width: 2rem;
height: 1rem;
}
.apply-container{
min-height: calc(100% -18rem);
background-color: #f5f5f5;
}
.banner {
height: 54.8rem;
background: url('@/assets/images/award/apply_bg.png') no-repeat;
background: url('@/assets/images/award/form_bg.png') no-repeat;
background-size: 100% 100%;
text-align: center;
padding: 12rem 21.4rem 0;
@@ -888,7 +1043,8 @@
}
}
:deep(.ant-input) {
:deep(.ant-input),
:deep(.ant-select-selector) {
border: 0.2rem solid #d5d5d5;
height: 6rem;
border-radius: 0.8rem;
@@ -899,6 +1055,20 @@
&.textarea {
height: 20rem;
}
.ant-select-selection-search {
height: 100%;
}
.ant-select-selection-item {
line-height: 6rem;
}
}
:deep(.ant-select-arrow) {
height: 4rem;
width: 6.2rem;
justify-content: center;
display: flex;
align-items: center;
border-left: 0.1rem solid #d5d5d5;
}
}
}
@@ -1092,7 +1262,6 @@
:deep(.ant-checkbox-wrapper) {
.ant-checkbox-inner {
//修改边框的颜色
width: 2rem;
height: 2rem;
border: 0.2rem solid #585858 !important;
@@ -1100,17 +1269,11 @@
}
.ant-checkbox-checked .ant-checkbox-inner {
//修改选中框的背景颜色
background-color: #fff !important;
/* 将背景颜色修改为白色 */
//修改边框颜色
border-color: #585858 !important;
/* 将边框颜色修改为黑色 */
}
.ant-checkbox-checked .ant-checkbox-inner::after {
//antd的checkbox组件的选中框里面的透明的钩子是通过设置底部边框和右边框的颜色再旋转得到的钩子
// 所以设置底部边框和右边框的样式就可以修改钩子的样式
border-bottom: 0.2rem solid #585858;
border-right: 0.2rem solid #585858;
width: 0.65rem;
@@ -1202,6 +1365,7 @@
font-family: 'ArialBold';
font-weight: 700;
font-size: 1.6rem;
cursor: pointer;
}
.cutdown {

View File

@@ -0,0 +1,51 @@
<template>
<div class="success-container flex flex-col align-center">
<img
src="@/assets/images/award/successful.png"
alt=""
class="icon-img"
/>
<div class="title">Submission Successful</div>
<div class="desc">
<div>
Please review your submitted information in the AiDA in-platform message.
</div>
<div>
You may edit it if needed. Competition updates and results will be sent
via email.
</div>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped>
.success-container {
margin: 0 21.5rem;
padding: 10.6rem 27.3rem 0;
height: 50rem;
position: relative;
top: -16.8rem;
background-color: #fff;
border-radius: 0.8rem;
.icon-img {
width: 12rem;
height: 12rem;
}
.title {
font-family: 'PoppinsBold';
font-weight: 600;
font-size: 3rem;
color: #232323;
text-align: center;
margin: 2rem 0 4rem;
}
.desc {
color: #585858;
font-family: Arial;
font-weight: 400;
font-size: 2.4rem;
text-align: center;
}
}
</style>

View File

@@ -8,7 +8,7 @@
/>
</div>
<div class="header-right flex align-center">
<div class="text">Submit your Application</div>
<div class="text">{{ btnText }}</div>
<img
src="@/assets/images/award/arrow.png"
alt=""
@@ -89,12 +89,23 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
const showQRcode = ref(false)
const handleCloseQRcode = () => {
showQRcode.value = false
}
type BtnType = 'index' | 'form'
const btnType = ref<BtnType>('index')
const btnText = computed(() => {
if (btnType.value === 'index') {
return 'Submit your Application'
}
if (btnType.value === 'form') {
return 'Back to Introduction'
}
})
</script>
<style lang="less" scoped>
@@ -123,6 +134,7 @@
}
.header-right {
column-gap: 1rem;
cursor: pointer;
.text {
font-size: 1.6rem;
color: #fff;