Files
aida_front/src/views/AwardPage/apply.vue
2026-01-19 10:56:39 +08:00

792 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="apply-container">
<div class="banner">
<div class="slogan">BLOOM YOUR CREATIVITY AiDA GLOBAL FASHION AWARD 2026</div>
<div class="title poppins-medium">Application Form</div>
<div class="form-header">
<div class="form-title poppins-bold">Email Verification</div>
<div class="desc">AiDA Users Only</div>
</div>
</div>
<div class="form-container">
<div class="form-content">
<a-form
name="form"
ref="formRef"
layout="vertical"
:model="form"
:rules="rulesRef"
autocomplete="off">
<div class="email-box full-row">
<a-form-item name="email" required label="Email Address">
<div class="email-wrapper flex align-center">
<a-input v-model:value="form.email" />
<div
class="code-btn"
:class="{ disabled: isCountingDown }"
@click="handleSendCode">
{{ isCountingDown ? formatCountdown(countdown) : 'Send Code' }}
</div>
</div>
<div class="tips">
Please use the email address you registered with AiDA.
</div>
</a-form-item>
</div>
<div class="form-row full-row">
<div class="form-title poppins-bold">Personal Information</div>
<div class="desc">Tell us about yourself</div>
</div>
<div class="user-info flex">
<template v-for="item in formKeys" :key="item.key">
<a-form-item
v-if="item.key !== 'email'"
:required="item.required"
:label="item.label"
:name="item.key">
<a-input v-model:value="form[item.key]" />
</a-form-item>
</template>
</div>
<div class="form-row full-row">
<div class="form-title poppins-bold">Design Information</div>
<div class="desc">Share your creative vision</div>
</div>
<a-form-item class="full-row design-title" name="designTitle" label="Design Title" required>
<a-input v-model:value="form.designTitle" />
</a-form-item>
<a-form-item class="full-row design-desc" name="description" label="Design description" required>
<a-textarea class="textarea" v-model:value="form.description"
placeholder="Briefly describe your design concept, inspiration, and creative direction..." />
</a-form-item>
<div class="form-row full-row">
<div class="form-title poppins-bold">Submission Files</div>
<div class="desc">Upload your design materials</div>
</div>
<div class="information full-row">
<div class="information-title flex align-center">
<div class="point"></div>
<div class="text poppins-bold">Submission Requirements</div>
</div>
<ul class="information-list flex space-between">
<li class="information-item">
{{
` Single PDF file\n Title, mood board, elaboration\n+ 4 outfit design with materials (max 15 pages)`
}}
</li>
<div class="right">
<li class="information-item">
Format: Single PDF file, 15pages, maximum 20MB
</li>
<li class="information-item">
{{
` Video: Design process, 1080×1920 pixels (9:16 ratio),\nmaximum 60 seconds`
}}
</li>
</div>
</ul>
</div>
<div class="upload-container full-row">
<a-form-item class="full-row" name="pdfFile" required label="How will you use AiDA in your design process?">
<a-upload-dragger v-model:fileList="pdfList" name="file" action=""
@change="info => handleFileChange(info, 'pdf')">
<img src="@/assets/images/award/upload.png" alt="" class="upload-icon" />
<p class="desc">Click to upload or drag and drop</p>
<p class="limit">PDF file, max 20MB</p>
</a-upload-dragger>
</a-form-item>
</div>
<div class="upload-container full-row">
<a-form-item class="full-row" name="videoFile" required
label="How will you use AiDA in your design process?">
<a-upload-dragger v-model:fileList="videoList" name="file" action=""
@change="info => handleFileChange(info, 'video')">
<img src="@/assets/images/award/upload.png" alt="" class="upload-icon" />
<p class="desc">Click to upload or drag and drop</p>
<p class="limit">Video file(MP4, MOV), max 20MB</p>
</a-upload-dragger>
</a-form-item>
</div>
</a-form>
<div class="conditions">
<div class="confitions-title poppins-bold">Terms & Conditions</div>
<div class="condition-list flex flex-col">
<div class="condition-item flex align-center" v-for="item in conditionsList" :key="item.id">
<a-checkbox v-model:checked="item.check" />
<span>{{ item.text }}</span>
</div>
</div>
</div>
</div>
</div>
<a-modal v-model:visible="showModal" :footer="null" :maskClosable="false" :closable="false"
wrapClassName="code-modal"
forceRender :keyboard="false" :style="{ top: '29.3rem' }">
<div class="verify-container flex flex-col align-center">
<img src="@/assets/images/award/close.svg" class="close-icon" @click="handleCloseModal" />
<div class="title poppins-bold">Check your email</div>
<div class="desc">Enter the 6-digital code sent to</div>
<div class="email">{{ form.email }}</div>
<div class="code-box">
<VerifycationCodeInput :ct="emailCode" @sendCaptcha="setVerifyCode" />
</div>
<div class="verify-btn" @click="handleVerifyCode">Verify</div>
<div class="cutdown" :class="{ disabled: isCountingDown }" @click="handleSendCode">
{{ isCountingDown ? `Resend Code in ${formatCountdown(countdown)}` : 'Resend' }}
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onUnmounted } from 'vue'
import { debounce } from 'lodash-es'
import type { Rule } from 'ant-design-vue/es/form'
import { message } from 'ant-design-vue'
import type { UploadChangeParam } from 'ant-design-vue'
// import VerificationCodeInput from './components/VerificationCodeInput.vue'
import VerifycationCodeInput from './components/verificationCodeInput.vue'
const formRef = ref(null)
const form = ref({
email: '',
firstName: '',
lastName: '',
gender: '',
occupation: '',
age: '',
counterOrRegion: '',
phone: '',
portfoilo: '',
// code: '',
designTitle: '',
description: '',
pdfFile: null,
videoFile: null
})
// 验证码输入组件引用
const codeInputRef = ref()
const emailCode = ref(['', '', '', '', '', ''])
const isValidEmail = email => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email)
}
const validEmail = (rule: Rule, value: string) => {
if (!value) {
return Promise.reject('Please input the email address')
}
if (!isValidEmail(value)) {
return Promise.reject('Please input a valid email address')
}
return Promise.resolve()
}
const validateNumber = async (_rule: Rule, value: string) => {
if (value === '') {
return Promise.reject('Please fill in this section.')
}
if (!Number.isInteger(value)) {
return Promise.reject('Please input digits')
}
return Promise.resolve()
}
const validatePhone = async (_rule: Rule, value: string) => {
if (value === '') {
return Promise.reject('Please enter your phone number.')
}
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/
if (!phoneRegex.test(value)) {
return Promise.reject('Please enter a valid phone number.')
}
return Promise.resolve()
}
const validateVerificationCode = async (_rule: Rule, value: string) => {
if (value === '') {
return Promise.reject('Please enter the verification code.')
}
const codeRegex = /^\d{6}$/
if (!codeRegex.test(value)) {
return Promise.reject('Please enter a 6-digit verification code.')
}
return Promise.resolve()
}
const rulesRef = {
email: [{ required: true, validator: validEmail }],
firstName: [
{ required: true, message: 'Please input your first name', trigger: 'blur' }
],
lastName: [{ required: true, message: 'Please input your last name', trigger: 'blur' }]
}
const formKeys = ref([
{
label: 'First Name',
required: true,
type: 'input',
key: 'firstName'
},
{
label: 'Last Name',
required: true,
type: 'input',
key: 'lastName'
},
{
label: 'Gender',
required: true,
type: 'select',
key: 'gender'
},
{
label: 'Occupation',
required: true,
type: 'input',
key: 'occupation'
},
{
label: 'Age',
required: true,
type: 'input',
key: 'age'
},
{
label: 'Country/Region and City',
required: true,
type: 'input',
key: 'counterOrRegion'
},
{
label: 'Phone Number',
required: true,
type: 'input',
key: 'phone'
},
{
label: 'Portfoilo Website/Instagram(Optional)',
required: false,
type: 'input',
key: 'portfoilo'
}
])
const showModal = ref(false)
// 倒计时相关状态
const countdown = ref(0)
const countdownTimer = ref<NodeJS.Timeout | null>(null)
const isCountingDown = ref(false)
const formatCountdown = (seconds: number) => {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds
.toString()
.padStart(2, '0')}`
}
const startCountdown = () => {
countdown.value = 60
isCountingDown.value = true
countdownTimer.value = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearCountdown()
}
}, 1000)
}
const clearCountdown = () => {
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
countdown.value = 0
isCountingDown.value = false
}
const handleSendCode = debounce(async () => {
// 如果正在倒计时,不允许再次发送
if (isCountingDown.value) {
return
}
try {
await formRef.value.validateFields(['email'])
// TODO: 发送验证码的逻辑
message.success('Verification code sent successfully!')
// 开始倒计时
startCountdown()
showModal.value = true
} catch (error) {}
}, 300)
const verifyCode = ref(null)
const setVerifyCode = value => {
verifyCode.value = value
}
const handleCloseModal = () => {
showModal.value = false
}
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
}
const pdfList = ref([])
const videoList = ref([])
type FileType = 'pdf' | 'video'
const handleFileChange = (info: UploadChangeParam, type: FileType) => {
console.log('change---------')
const status = info.file.status
if (status !== 'uploading') {
console.log('file:', info.file)
}
if (status === 'done') {
message.success(`${info.file.name} file uploaded successfully.`)
if (type === 'pdf') {
}
} else if (status === 'error') {
message.error(`${info.file.name} file upload failed.`)
}
}
const conditionsList = ref([
{
check: false,
text: 'I confirm that all submitted work is original and created by me.',
id: 'first'
},
{
check: false,
text: 'I understand that Code-Create has marketing and promotional rights to all submitted designs and videos.',
id: 'second'
},
{
check: false,
text: 'I agree to participate in finalist activities if selected, including AiDA training and award ceremony.',
id: 'third'
},
{
check: false,
text: 'I would like to receive updates about AiDA products and future competitions. (Optional)',
id: 'forth'
}
])
// 组件卸载时清理定时器
onUnmounted(() => {
clearCountdown()
})
</script>
<style lang="less" scoped>
.poppins-bold {
font-family: 'PoppinsBold';
font-weight: 600;
}
.poppins-medium {
font-family: 'PoppinsMedium';
font-weight: 500;
}
.full-row {
width: 100%;
}
.banner {
height: 54.8rem;
background: url('@/assets/images/award/apply_bg.png') no-repeat;
background-size: 100% 100%;
text-align: center;
padding: 12rem 21.4rem 0;
position: relative;
.slogan {
color: #585858;
font-family: 'ArialBold';
font-weight: 700;
font-size: 2.8rem;
margin-bottom: 4rem;
}
.title {
color: #c7342c;
font-size: 8rem;
}
.form-header {
height: 16.8rem;
width: calc(100% - 42.8rem);
left: 21.4rem;
bottom: 0;
position: absolute;
background-color: #fff;
border-top-left-radius: 0.8rem;
border-top-right-radius: 0.8rem;
text-align: left;
padding: 6rem 6rem 0;
.form-title {
color: #232323;
font-size: 3rem;
margin-bottom: 1rem;
}
.desc {
color: #b10000;
// font-family: 'Instrument';
font-family: revert-layer;
font-weight: 500;
font-size: 2.4rem;
}
}
}
.form-row {
margin-top: 12rem;
margin-bottom: 6rem;
.form-title {
color: #232323;
font-size: 3rem;
margin-bottom: 1rem;
}
.desc {
color: #b10000;
// font-family: 'Instrument';
font-family: revert-layer;
font-weight: 500;
font-size: 2.4rem;
}
}
.form-container {
padding: 0 21.4rem 12rem;
background-color: #f5f5f5;
.form-content {
padding: 4rem 6rem 6rem;
background-color: #fff;
border-bottom-left-radius: 0.8rem;
border-bottom-right-radius: 0.8rem;
.ant-form {
display: flex;
flex-wrap: wrap;
column-gap: 4rem;
.user-info {
flex-wrap: wrap;
row-gap: 6rem;
justify-content: space-between;
}
.ant-form-item {
width: 66.5rem;
margin-bottom: 0;
&.full-row {
width: 100%;
}
&.design-title {
margin-bottom: 9rem;
}
:deep(label) {
// display: none;
flex-direction: row-reverse;
color: #232323;
&,
&::before {
font-family: 'Arial';
font-weight: 400;
font-size: 2rem;
}
}
:deep(.ant-input) {
border: 0.2rem solid #d5d5d5;
height: 6rem;
border-radius: 0.8rem;
font-family: 'Arial';
font-weight: 400;
font-size: 1.8rem;
&.textarea {
height: 20rem;
}
}
}
}
.email-box {
:deep(.ant-input) {
border: none !important;
&:focus {
box-shadow: none;
}
}
.email-wrapper {
border-radius: 0.8rem;
border: 0.2rem solid #d5d5d5;
}
.code-btn {
width: 13rem;
height: 4rem;
color: #585858;
font-family: 'Arial';
font-weight: 400;
font-size: 1.8rem;
line-height: 4rem;
text-align: center;
cursor: pointer;
border-left: 0.1rem solid #d5d5d5;
&.disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
}
.tips {
font-family: 'Arial';
font-weight: 400;
font-size: 1.6rem;
color: #585858;
margin-top: 1rem;
}
}
}
:deep(.ant-form-item-label) {
padding: 0 0 1rem;
}
.information {
padding-left: 2.4rem;
.information-title {
font-size: 2.8rem;
color: #232323;
column-gap: 2.4rem;
margin-bottom: 5rem;
.point {
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
background-color: #c7342c;
}
}
.information-list {
padding-left: 2.4rem;
column-gap: 4.8rem;
.information-item {
font-family: 'Arial';
font-weight: 400;
font-size: 2.4rem;
color: #585858;
position: relative;
list-style: disc;
// line-height: 3rem;
white-space: pre-line;
}
}
}
.upload-container {
margin-top: 6rem;
:deep(.ant-upload-drag) {
height: 32rem;
.ant-upload-btn {
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 5.89rem;
.upload-icon {
width: 12rem;
height: 12rem;
}
.desc {
color: #585858;
font-family: 'Arial';
font-weight: 400;
font-size: 2.4rem;
margin: 1rem 0 2rem;
}
.limit {
color: #aaa;
font-family: 'Arial';
font-weight: 400;
font-size: 1.8rem;
}
}
}
}
.conditions {
margin-top: 12rem;
&-title {
color: #232323;
font-size: 3rem;
}
.condition-list {
margin-top: 6rem;
row-gap: 3rem;
.condition-item {
border: 0.2rem solid #d5d5d5;
height: 6rem;
line-height: 6rem;
border-radius: 0.8rem;
color: #585858;
font-family: 'Arial';
font-weight: 400;
font-size: 1.8rem;
padding-left: 1.5rem;
column-gap: 2.5rem;
}
:deep(.ant-checkbox-inner) {
border: 0.2rem solid #585858;
}
:deep(.ant-checkbox-wrapper:hover .ant-checkbox-inner,
.ant-checkbox:hover .ant-checkbox-inner,
.ant-checkbox-input:focus + .ant-checkbox-inner) {
border-color: #585858 !important;
}
:deep(.ant-checkbox-wrapper:hover .ant-checkbox-inner,
.ant-checkbox:hover .ant-checkbox-inner,
.ant-checkbox-input:focus+.ant-checkbox-inner) {
border-color: #585858 !important;
}
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
border-color: #000 !important;
background-color: #000 !important;
}
}
}
.verify-container {
width: 60rem;
height: 49.4rem;
padding: 6rem 8.6rem;
background-color: #fff;
position: relative;
.close-icon {
position: absolute;
width: 2.4rem;
height: 2.4rem;
top: 2rem;
right: 2rem;
cursor: pointer;
}
.title {
color: #232323;
font-size: 3rem;
line-height: 5rem;
}
.desc {
color: #585858;
font-family: 'Arial';
font-weight: 400;
font-size: 1.6rem;
line-height: 3.4rem;
margin-top: 1.4rem;
}
.email {
font-family: 'ArialBold';
font-weight: 700;
font-size: 1.6rem;
line-height: 3rem;
color: #232323;
}
.code-box {
width: 100%;
margin: 5rem 0;
}
.verify-btn {
background-color: #232323;
height: 4.2rem;
border-radius: 8px;
line-height: 4.2rem;
width: 100%;
color: #fff;
text-align: center;
font-family: 'ArialBold';
font-weight: 700;
font-size: 1.6rem;
}
.cutdown {
font-family: 'Arial';
font-weight: 400;
font-size: 1.4rem;
color: #585858;
margin-top: 2rem;
text-decoration: underline solid #585858;
&:not(.disabled) {
cursor: pointer;
}
&.disabled {
cursor: not-allowed;
}
}
}
</style>
<style lang="less">
.code-modal {
.ant-modal-content .ant-modal-body {
padding: 0;
// width: 60rem;
}
}
</style>