style: 表单页面移动端

This commit is contained in:
2026-03-24 11:12:49 +08:00
parent 982b7308e8
commit 341cbf7eb1
8 changed files with 676 additions and 230 deletions

View File

@@ -1,19 +1,51 @@
<template>
<div class="apply-container">
<div class="banner">
<div class="slogan">
<div class="apply-container" :class="{ mobile: isMobile }">
<div class="banner" :class="{ tablet: isTablet, mobile: isMobile && !isTablet }">
<div class="slogan" @click="handleTestDate">
{{ t('AwardApply.slogan') }}
</div>
<div class="title poppins-medium">{{ t('AwardApply.applicationForm') }}</div>
<div class="form-header" v-if="!isCompleted && !isExpired">
<div class="title poppins-medium" @click="handleTestComplete">
{{ t('AwardApply.applicationForm') }}
</div>
<div class="form-header" v-if="!isCompleted && !isExpired && !isMobile && !isTablet">
<div class="form-title poppins-bold">
{{ t('AwardApply.emailVerification') }}
</div>
<div class="desc">{{ t('AwardApply.aidaUsersOnly') }}</div>
</div>
<div class="steps-container" v-if="(isMobile || isTablet) && !isCompleted && !isExpired">
<Step v-model="step" />
</div>
</div>
<Success :isExpired="isExpired" v-if="isCompleted || isExpired" />
<div class="form-container" v-if="!isCompleted && !isExpired">
<div
class="success-wrapper"
v-if="isCompleted || isExpired"
:class="{ mobile: isMobile, tablet: isTablet }"
>
<Success
class="success-container"
:class="{ mobile: isMobile, tablet: isTablet }"
:isExpired="isExpired"
/>
<div
class="step-btn back"
v-show="(isCompleted || isExpired) && (isMobile || isTablet)"
@click="handleBackToIntroduction"
>
{{ t('AwardApply.backToIntroduction') }}
</div>
</div>
<div
class="form-container"
v-if="!isCompleted && !isExpired"
:class="{ mobile: isMobile, tablet: isTablet }"
>
<div class="form-header" v-if="showStep(0)">
<div class="form-title poppins-bold">
{{ t('AwardApply.emailVerification') }}
</div>
<div class="desc">{{ t('AwardApply.aidaUsersOnly') }}</div>
</div>
<div class="form-content">
<a-form
name="form"
@@ -23,7 +55,7 @@
:rules="rulesRef"
autocomplete="off"
>
<div class="email-box full-row">
<div class="email-box full-row" v-show="showStep(0)">
<a-form-item name="email" required :label="t('AwardApply.emailAddress')">
<div class="email-wrapper flex align-center">
<a-input v-model:value="form.email" />
@@ -47,13 +79,36 @@
</div>
</a-form-item>
</div>
<div class="form-row full-row">
<div
v-if="isMobile || isTablet"
class="email-box verify-box full-row"
v-show="showStep(0)"
>
<a-form-item name="verify" required :label="t('AwardApply.verifyCode')">
<div class="email-wrapper flex align-center">
<a-input
v-model:value="mobileVerifyCode"
:placeholder="t('AwardApply.verifyPlaceholder')"
/>
<div class="code-btn" @click="handleVerifyCode">
{{ t('AwardApply.verify') }}
</div>
<img
v-if="hasValidEmail"
src="@/assets/images/award/checked.png"
alt=""
class="checked-icon"
/>
</div>
</a-form-item>
</div>
<div v-show="showStep(1)" class="form-row full-row" :class="{ mobile: isMobile }">
<div class="form-title poppins-bold">
{{ t('AwardApply.personalInformation') }}
</div>
<div class="desc">{{ t('AwardApply.tellUsAboutYourself') }}</div>
</div>
<div class="user-info flex">
<div v-show="showStep(1)" class="user-info flex">
<template v-for="item in formKeys" :key="item.key">
<a-form-item
v-if="item.key !== 'email'"
@@ -74,9 +129,9 @@
>
<template #suffixIcon>
<!-- <img
class="arrow-down-icon"
src="@/assets/images/award/arrow_down.svg"
/> -->
class="arrow-down-icon"
src="@/assets/images/award/arrow_down.svg"
/> -->
</template>
</a-select>
<div class="arrow-wrapper flex flex-center">
@@ -86,176 +141,182 @@
</a-form-item>
</template>
</div>
<div class="form-row full-row">
<div class="form-title poppins-bold">
{{ t('AwardApply.designInformation') }}
</div>
<div class="desc">
{{ t('AwardApply.shareYourCreativeVision') }}
</div>
</div>
<a-form-item
class="full-row design-title"
name="designTitle"
:label="t('AwardApply.designTitle')"
required
>
<a-input v-model:value="form.designTitle" :disabled="readOnly" />
</a-form-item>
<a-form-item
class="full-row design-desc"
name="designDescription"
:label="t('AwardApply.designDescription')"
required
>
<a-textarea
class="textarea"
:disabled="readOnly"
v-model:value="form.designDescription"
:placeholder="t('AwardApply.designDescriptionPlaceholder')"
/>
</a-form-item>
<div class="form-row full-row">
<div class="form-title poppins-bold">
{{ t('AwardApply.submissionFiles') }}
</div>
<div class="desc">
{{ t('AwardApply.uploadYourDesignMaterials') }}
</div>
</div>
<div class="information full-row">
<div class="information-title flex align-center">
<div class="point"></div>
<div class="text poppins-bold">
{{ t('AwardApply.submissionRequirements') }}
<template v-if="showStep(2)">
<div class="form-row full-row">
<div class="form-title poppins-bold">
{{ t('AwardApply.designInformation') }}
</div>
<div class="desc">
{{ t('AwardApply.shareYourCreativeVision') }}
</div>
</div>
<ul class="information-list flex space-between">
<li class="information-item">
{{ t('AwardApply.pdfRequirement') }}
</li>
<div class="right">
<li class="information-item">
{{ t('AwardApply.rightContent.format') }}
</li>
<li class="information-item">
{{ t('AwardApply.rightContent.video') }}
</li>
</div>
</ul>
</div>
<div class="upload-container full-row">
<a-form-item
class="full-row"
name="pdfPath"
class="full-row design-title"
name="designTitle"
:label="t('AwardApply.designTitle')"
required
:validate-trigger="[]"
:label="t('AwardApply.uploadPdfTitle')"
>
<div>
<a-upload-dragger
v-model:fileList="pdfList"
:disabled="readOnly"
:maxCount="1"
:showUploadList="false"
@change="(info) => handleFileChange(info, 'pdf')"
:customRequest="handleUploadPdf"
:beforeUpload="beforeUploadPdf"
@remove="handleRemoveFile('pdf')"
accept=".pdf"
>
<img src="@/assets/images/award/upload.png" alt="" class="upload-icon" />
<p class="desc">
{{ t('AwardApply.clickToUploadPdf') }}
</p>
<p class="limit">
{{ t('AwardApply.pdfFileLimit') }}
</p>
</a-upload-dragger>
<!-- 自定义文件列表显示 -->
<div
v-if="pdfUploadStatus === 'success' && pdfList.length > 0"
class="custom-upload-list flex align-center space-between"
>
<div class="flex align-center">
<SvgIcon name="CFile" />
{{ pdfList[0]?.name }}
</div>
<div @click="handleRemoveFile('pdf')" class="delete-file" title="delete file">
<SvgIcon name="CDelete" color="red" />
</div>
</div>
</div>
<div
v-show="pdfUploadStatus === 'uploading' || pdfUploadStatus === 'success'"
class="uploading-container"
>
<UploadStatus :status="pdfUploadStatus" type="pdf" />
<div class="progress-bar-container" v-if="pdfUploadStatus === 'uploading'">
<div class="progress-bar" :style="{ width: `${uploadProgressPdf}%` }"></div>
</div>
</div>
<a-input v-model:value="form.designTitle" :disabled="readOnly" />
</a-form-item>
</div>
<div class="upload-container full-row">
<a-form-item
class="full-row"
name="videoPath"
class="full-row design-desc"
name="designDescription"
:label="t('AwardApply.designDescription')"
required
:validate-trigger="[]"
:label="t('AwardApply.uploadVideoTitle')"
>
<div>
<a-upload-dragger
v-model:fileList="videoList"
:disabled="readOnly"
:maxCount="1"
:showUploadList="false"
@change="(info) => handleFileChange(info, 'video')"
:customRequest="handleUploadVideo"
:beforeUpload="beforeUploadVideo"
@remove="handleRemoveFile('video')"
accept=".mp4"
>
<img
src="@/assets/images/award/upload_video_icon.png"
alt=""
class="upload-icon"
/>
<p class="desc">
{{ t('AwardApply.clickToUploadVideo') }}
</p>
<p class="limit">
{{ t('AwardApply.videoFileLimit') }}
</p>
</a-upload-dragger>
<!-- 自定义文件列表显示 -->
<div
v-if="videoUploadStatus === 'success' && videoList.length > 0"
class="custom-upload-list flex align-center space-between"
>
<div class="flex align-center">
<SvgIcon name="CFile" />
{{ videoList[0]?.name }}
</div>
<div @click="handleRemoveFile('video')" class="delete-file" title="delete file">
<SvgIcon name="CDelete" color="red" />
</div>
</div>
</div>
<div
v-show="videoUploadStatus === 'success' || videoUploadStatus === 'uploading'"
class="uploading-container"
>
<UploadStatus :status="videoUploadStatus" type="video" />
<div class="progress-bar-container" v-if="videoUploadStatus === 'uploading'">
<div class="progress-bar" :style="{ width: `${uploadProgressVideo}%` }"></div>
</div>
</div>
<a-textarea
class="textarea"
:disabled="readOnly"
v-model:value="form.designDescription"
:placeholder="t('AwardApply.designDescriptionPlaceholder')"
/>
</a-form-item>
</div>
<div class="form-row full-row">
<div class="form-title poppins-bold">
{{ t('AwardApply.submissionFiles') }}
</div>
<div class="desc">
{{ t('AwardApply.uploadYourDesignMaterials') }}
</div>
</div>
<div class="information full-row">
<div class="information-title flex align-center">
<div class="point"></div>
<div class="text poppins-bold">
{{ t('AwardApply.submissionRequirements') }}
</div>
</div>
<ul class="information-list flex space-between">
<li class="information-item">
{{ t('AwardApply.pdfRequirement') }}
</li>
<div class="right">
<li class="information-item">
{{ t('AwardApply.rightContent.format') }}
</li>
<li class="information-item">
{{ t('AwardApply.rightContent.video') }}
</li>
</div>
</ul>
</div>
<div class="upload-container full-row">
<a-form-item
class="full-row"
name="pdfPath"
required
:validate-trigger="[]"
:label="t('AwardApply.uploadPdfTitle')"
>
<div>
<a-upload-dragger
v-model:fileList="pdfList"
:disabled="readOnly"
:maxCount="1"
:showUploadList="false"
@change="(info) => handleFileChange(info, 'pdf')"
:customRequest="handleUploadPdf"
:beforeUpload="beforeUploadPdf"
@remove="handleRemoveFile('pdf')"
accept=".pdf"
>
<img src="@/assets/images/award/upload.png" alt="" class="upload-icon" />
<p class="desc">
{{ t('AwardApply.clickToUploadPdf') }}
</p>
<p class="limit">
{{ t('AwardApply.pdfFileLimit') }}
</p>
</a-upload-dragger>
<!-- 自定义文件列表显示 -->
<div
v-if="pdfUploadStatus === 'success' && pdfList.length > 0"
class="custom-upload-list flex align-center space-between"
>
<div class="flex align-center">
<SvgIcon name="CFile" />
{{ pdfList[0]?.name }}
</div>
<div @click="handleRemoveFile('pdf')" class="delete-file" title="delete file">
<SvgIcon name="CDelete" color="red" />
</div>
</div>
</div>
<div
v-show="pdfUploadStatus === 'uploading' || pdfUploadStatus === 'success'"
class="uploading-container"
>
<UploadStatus :status="pdfUploadStatus" type="pdf" />
<div class="progress-bar-container" v-if="pdfUploadStatus === 'uploading'">
<div class="progress-bar" :style="{ width: `${uploadProgressPdf}%` }"></div>
</div>
</div>
</a-form-item>
</div>
<div class="upload-container full-row">
<a-form-item
class="full-row"
name="videoPath"
required
:validate-trigger="[]"
:label="t('AwardApply.uploadVideoTitle')"
>
<div>
<a-upload-dragger
v-model:fileList="videoList"
:disabled="readOnly"
:maxCount="1"
:showUploadList="false"
@change="(info) => handleFileChange(info, 'video')"
:customRequest="handleUploadVideo"
:beforeUpload="beforeUploadVideo"
@remove="handleRemoveFile('video')"
accept=".mp4"
>
<img
src="@/assets/images/award/upload_video_icon.png"
alt=""
class="upload-icon"
/>
<p class="desc">
{{ t('AwardApply.clickToUploadVideo') }}
</p>
<p class="limit">
{{ t('AwardApply.videoFileLimit') }}
</p>
</a-upload-dragger>
<!-- 自定义文件列表显示 -->
<div
v-if="videoUploadStatus === 'success' && videoList.length > 0"
class="custom-upload-list flex align-center space-between"
>
<div class="flex align-center">
<SvgIcon name="CFile" />
{{ videoList[0]?.name }}
</div>
<div @click="handleRemoveFile('video')" class="delete-file" title="delete file">
<SvgIcon name="CDelete" color="red" />
</div>
</div>
</div>
<div
v-show="videoUploadStatus === 'success' || videoUploadStatus === 'uploading'"
class="uploading-container"
>
<UploadStatus :status="videoUploadStatus" type="video" />
<div class="progress-bar-container" v-if="videoUploadStatus === 'uploading'">
<div class="progress-bar" :style="{ width: `${uploadProgressVideo}%` }"></div>
</div>
</div>
</a-form-item>
</div>
</template>
</a-form>
<div class="conditions">
<div
class="conditions"
:class="{ mobile: isMobile, tablet: isTablet }"
v-show="showStep(3)"
>
<div class="confitions-title poppins-bold">
{{ t('AwardApply.termsAndConditions') }}
</div>
@@ -273,7 +334,8 @@
</div>
</div>
</div>
<div class="submit-container">
<div class="submit-container" v-if="showStep(3)">
<div class="submit-btn poppins-bold" @click="handleSubmitForm">
{{ t('AwardApply.submitYourDesign') }}
</div>
@@ -282,6 +344,12 @@
</div>
</div>
</div>
<div v-if="(isMobile || isTablet) && step < 3" class="step-container">
<div class="step-btn" @click="handleNextStep">
{{ t('AwardApply.nextStep') }}
</div>
<div class="step-tips">{{ t('AwardApply.stepTips') }}</div>
</div>
</div>
<a-modal
v-model:visible="showModal"
@@ -321,6 +389,7 @@
<script setup lang="ts">
import { ref, onUnmounted, onMounted, computed, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { debounce } from 'lodash-es'
import type { Rule } from 'ant-design-vue/es/form'
@@ -328,22 +397,52 @@ import { message, Upload } from 'ant-design-vue'
import { Https } from '@/utils/request'
import { useRoute } from 'vue-router'
import type { UploadChangeParam } from 'ant-design-vue'
import { useIsMobile, useIsTablet } from '@/utils/isMobile'
import VerifycationCodeInput from './components/VerificationCodeInput.vue'
import UploadStatus from './components/UploadStatus.vue'
import Success from './components/Success.vue'
import Step from './components/Step.vue'
const router = useRouter()
const { isMobile } = useIsMobile()
const { isTablet } = useIsTablet()
const step = ref(0)
const showStep = (val: number) => {
if (!isMobile.value && !isTablet.value) {
return true
} else {
return step.value === val
}
}
// 是否晚于2026年7月15日
const date = ref('2026-07-15')
const isExpired = computed(() => {
const now = new Date()
const targetDate = new Date('2026-07-15')
const targetDate = new Date(date.value)
return now > targetDate
})
const { t } = useI18n()
const route = useRoute()
const isCompleted = ref(false)
let dateClick = 0
const handleTestDate = () => {
dateClick++
if (dateClick % 2 === 0) {
date.value = date.value === '2026-07-15' ? '2026-01-15' : '2026-07-15'
}
}
let completeClick = 0
const handleTestComplete = () => {
completeClick++
if (completeClick % 4 === 0) {
isCompleted.value = !isCompleted.value
}
}
const readOnly = computed(() => {
if (route.query.id && !hasValidEmail.value) {
return true
@@ -633,11 +732,14 @@ const handleSendCode = debounce(async () => {
startCountdown()
emailCode.value = ['', '', '', '', '', ''] // 重置验证码输入
showModal.value = true
if (!isMobile.value && !isTablet.value) {
showModal.value = true
}
} catch (error) {}
}, 300)
const verifyCode = ref(null)
const mobileVerifyCode = ref('')
const setVerifyCode = (value) => {
verifyCode.value = value
@@ -646,9 +748,14 @@ const setVerifyCode = (value) => {
const handleCloseModal = () => {
showModal.value = false
}
const handleVerifyCode = () => {
const handleVerifyCode = async () => {
// console.log(verifyCode.value)
if (isMobile.value || isTablet.value) {
await formRef.value.validateFields(['email'])
}
if (isMobile.value || isTablet.value) {
verifyCode.value = mobileVerifyCode.value
}
if (verifyCode.value.length !== 6) {
message.error(t('AwardApply.pleaseEnterCompleteCode'))
return
@@ -1071,6 +1178,16 @@ const handleEchoForm = () => {
})
}
const handleNextStep = () => {
if (step.value < 3) {
step.value++
}
}
const handleBackToIntroduction = () => {
router.push('/')
}
onMounted(() => {
if (route.query.id) {
handleEchoForm()
@@ -1104,7 +1221,22 @@ onUnmounted(() => {
background-color: #f5f5f5;
box-sizing: border-box;
}
.success-wrapper {
padding-bottom: 3rem;
.success-container {
&.mobile {
height: auto;
top: initial;
margin: 4rem 3.8rem 0;
padding: 13.5rem 13.2rem;
}
}
&.tablet {
.step-btn {
margin: 6rem 23.3rem 0;
}
}
}
.banner {
height: 54.8rem;
background: url('@/assets/images/award/form_bg.png') no-repeat;
@@ -1125,37 +1257,62 @@ onUnmounted(() => {
color: #c7342c;
font-size: 8rem;
}
.form-header {
height: 16.8rem;
width: calc(100% - 42.8rem);
left: 21.4rem;
bottom: 0;
position: absolute;
}
.steps-container {
height: 6.8rem;
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;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
}
&.mobile {
height: 24.8rem;
background: url('@/assets/images/mobile_version_background/banner_bg.png') no-repeat;
background-size: 100% 100%;
padding: 4.1rem 6rem 0;
.slogan {
font-size: 1.8rem;
margin-bottom: 1.2rem;
}
.desc {
color: #b10000;
font-family: 'Instrument';
font-weight: 500;
font-size: 2.4rem;
.title {
font-size: 4rem;
}
}
}
.form-header {
height: 16.8rem;
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-weight: 500;
font-size: 2.4rem;
}
}
.form-row {
margin-top: 12rem;
margin-bottom: 6rem;
&.mobile {
margin-top: 0;
}
.form-title {
color: #232323;
@@ -1307,6 +1464,58 @@ onUnmounted(() => {
margin-top: 1rem;
}
}
&.mobile,
&.tablet {
margin-top: 4rem;
.form-row {
margin-top: 0;
}
.form-title {
margin-top: 3rem;
}
}
&.mobile:not(.tablet) {
padding: 0 3.8rem 4rem;
.form-header {
padding: 3rem 3rem 0;
height: auto;
padding-bottom: 2.4rem;
}
.form-content {
padding: 0 3rem 20rem;
.verify-box {
margin-top: 4rem;
}
.ant-form .ant-form-item {
width: 100%;
}
}
}
}
.step-btn {
text-align: center;
margin: 6rem 3rem 0;
background-color: #454545;
height: 5rem;
border-radius: 0.8rem;
color: #fff;
line-height: 5rem;
font-family: 'PoppinsBold';
font-weight: 600;
font-size: 2.4rem;
cursor: pointer;
&.back {
margin: 6rem 6.6rem 0;
}
}
.step-tips {
text-align: center;
margin-top: 2rem;
font-family: 'Instrument';
font-weight: 400;
font-size: 1.8rem;
color: #6d6d6d;
}
:deep(.ant-form-item-label) {
@@ -1438,7 +1647,9 @@ onUnmounted(() => {
}
.conditions {
margin-top: 12rem;
&.mobile {
margin-top: 3rem;
}
&-title {
color: #232323;
font-size: 3rem;
@@ -1486,6 +1697,17 @@ onUnmounted(() => {
}
}
}
&.mobile:not(.tablet) {
.condition-list {
.condition-item {
align-items: flex-start;
height: auto;
min-height: 6rem;
line-height: 2.4rem;
padding: 2rem 1.5rem;
}
}
}
}
.submit-container {