Files
QSY-lvjuxcx/pkgAccount/register/register.vue
liangjian 1111ac148b feat: 重构核心租住流程页面与交互体验
统一注册、看房、申请与入住等核心页面的视觉和流程衔接,补齐支付、签约与状态流转闭环,提升移动端操作连贯性。
2026-07-29 09:05:38 +08:00

2202 lines
71 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>
<view class="register-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="true" :bottomShadow="false" :backgroundColor="navBackgroundColor">
<view class="nav-title">{{ $tr('注册账号') }}</view>
</tn-nav-bar>
<view class="register-content" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="ambient-orb orb-a"></view>
<view class="ambient-orb orb-b"></view>
<view class="ambient-grid"></view>
<view class="page-hero animation-fade-in">
<view class="hero-kicker">{{ $tr('绿居公寓登记入口') }}</view>
<view class="page-title">{{ $tr('注册并提交实名资料') }}</view>
<view class="page-desc">
{{ $tr('账号信息和实名认证资料需要一起填写完成,提交的是注册申请单,审批通过后才算注册成功。') }}
</view>
<view class="draft-tip-bar">
<text class="tn-icon-safe-fill draft-tip-icon"></text>
<text class="draft-tip-text">{{ draftTipText }}</text>
</view>
<view class="hero-badge-list">
<view class="hero-badge-card">
<view class="hero-badge-label">{{ $tr('流程') }}</view>
<view class="hero-badge-value">{{ $tr('4 个模块') }}</view>
</view>
<view class="hero-badge-card">
<view class="hero-badge-label">{{ $tr('保存') }}</view>
<view class="hero-badge-value">{{ $tr('自动暂存') }}</view>
</view>
<view class="hero-badge-card">
<view class="hero-badge-label">{{ $tr('结果') }}</view>
<view class="hero-badge-value">{{ $tr('审批后开通') }}</view>
</view>
</view>
</view>
<view class="progress-strip animation-fade-in" style="animation-delay: 0.08s;">
<view class="progress-node is-active">
<text class="progress-node-index">01</text>
<text class="progress-node-text">{{ $tr('账号') }}</text>
</view>
<view class="progress-node">
<text class="progress-node-index">02</text>
<text class="progress-node-text">{{ $tr('实名') }}</text>
</view>
<view class="progress-node">
<text class="progress-node-index">03</text>
<text class="progress-node-text">{{ $tr('档案') }}</text>
</view>
<view class="progress-node">
<text class="progress-node-index">04</text>
<text class="progress-node-text">{{ $tr('偏好') }}</text>
</view>
</view>
<view class="form-card animation-fade-in" style="animation-delay: 0.12s;">
<view class="section-head">
<view class="section-index">01</view>
<view class="section-copy">
<view class="section-title">{{ $tr('账号信息') }}</view>
<view class="section-desc">{{ $tr('用于登录和接收后续通知') }}</view>
</view>
</view>
<view class="section-body">
<view class="form-item">
<view class="form-label">{{ $tr('登录账号') }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="$tr('请输入登录账号')" placeholder-class="input-placeholder" v-model="form.account" />
</view>
<view class="form-item">
<view class="form-label">{{ $tr('登录密码') }} <text class="required-mark">*</text></view>
<input class="form-input" type="password" :placeholder="$tr('请输入 6 位及以上密码')" placeholder-class="input-placeholder" v-model="form.password" />
</view>
<view class="form-item" style="margin-bottom: 0;">
<view class="form-label">{{ $tr('邮箱') }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="$tr('请输入邮箱')" placeholder-class="input-placeholder" v-model="form.email" />
</view>
<!-- 暂时屏蔽邮箱验证码 -->
<view class="form-item" style="margin-bottom: 0;" v-if="false">
<view class="form-label">{{ $tr('邮箱验证码') }}</view>
<input class="form-input" :placeholder="$tr('请输入邮箱验证码')" placeholder-class="input-placeholder" v-model="form.emailVerifyCode" />
</view>
</view>
</view>
<view class="form-card animation-fade-in" style="animation-delay: 0.2s;">
<view class="section-head">
<view class="section-index">02</view>
<view class="section-copy">
<view class="section-title">{{ $tr('基础实名信息') }}</view>
<view class="section-desc">{{ $tr('根据身份展示对应的证件和字段') }}</view>
</view>
</view>
<view class="section-body">
<view class="identity-panel">
<view class="identity-panel-label">{{ $tr('身份类型') }}</view>
<view class="role-switch-wrap">
<view class="role-switch tn-flex">
<view class="role-tab tn-flex-1" :class="form.identityType === 'ForeignStudents' ? 'role-active' : ''" @click="switchIdentityType('ForeignStudents')">
<view>{{ $tr('外籍人员') }}</view>
<view v-if="language !== 'en'" class="role-tab-subtitle">{{ $tr('Foreign') }}</view>
</view>
<view class="role-tab tn-flex-1" :class="form.identityType === 'DomesticStudents' ? 'role-active' : ''" @click="switchIdentityType('DomesticStudents')">
<view>{{ $tr('中国籍') }}</view>
<view v-if="language !== 'en'" class="role-tab-subtitle">{{ $tr('Chinese') }}</view>
</view>
</view>
</view>
<view class="identity-panel-tip">{{ $tr('切换后会同步调整国籍、证件类型和上传要求') }}</view>
</view>
<view class="form-item">
<view class="form-label">{{ $tr('姓名') }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="$tr('请输入真实姓名')" placeholder-class="input-placeholder" v-model="form.name" />
</view>
<view class="form-item">
<view class="form-label">{{ $tr('性别') }} <text class="required-mark">*</text></view>
<view class="tn-flex">
<view
v-for="(item, index) in genderOptions"
:key="item.key"
class="gender-btn tn-flex-1"
:class="[form.gender === Number(item.key) ? 'gender-active' : '', index > 0 ? 'tn-margin-left-sm' : '']"
@click="form.gender = Number(item.key)"
>
{{ item.value }}
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">{{ $tr('手机号') }} <text class="required-mark">*</text></view>
<input class="form-input" type="number" :placeholder="$tr('请输入手机号')" placeholder-class="input-placeholder" v-model="form.phone" />
</view>
<view class="form-item" style="margin-bottom: 0;">
<view class="form-label">{{ $tr('国籍') }} <text class="required-mark">*</text></view>
<view v-if="isDomesticIdentity" class="form-input picker-input picker-input-disabled">
<text class="text-black">{{ form.country || $tr('中国') }}</text>
<text class="picker-fixed-text">{{ $tr('中国籍固定') }}</text>
</view>
<picker v-else mode="selector" :range="countryRange" @change="onCountryChange">
<view class="form-input picker-input">
<text :class="form.country ? 'text-black' : 'text-gray'">{{ form.country || $tr('请选择国籍') }}</text>
<text class="tn-icon-right picker-arrow"></text>
</view>
</picker>
</view>
</view>
</view>
<view class="form-card animation-fade-in" style="animation-delay: 0.28s;">
<view class="section-head">
<view class="section-index">03</view>
<view class="section-copy">
<view class="section-title">{{ $tr('认证档案') }}</view>
<view class="section-desc">{{ $tr('上传证件和补充资料,便于审核') }}</view>
</view>
</view>
<view class="section-body">
<view class="form-item">
<view class="form-label">{{ $tr('工作单位') }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="$tr('请输入工作单位')" placeholder-class="input-placeholder" v-model="form.workUnit" />
</view>
<view class="form-item">
<view class="form-label">{{ $tr('入学通知书(在职证明)附件') }}</view>
<view v-if="!form.admissionNoticeAttachment.serverValue" class="upload-box" @click="uploadAttachment('admissionNoticeAttachment')">
<view class="upload-placeholder">
<view class="upload-icon-wrap"><text class="tn-icon-upload"></text></view>
<view class="upload-text">{{ $tr('点击上传入学通知书/在职证明') }}</view>
<view class="upload-subtext">{{ $tr('建议上传清晰的图片版本') }}</view>
</view>
</view>
<view v-else class="attachment-file-card">
<image
class="attachment-preview-image"
:src="getAttachmentPreviewUrl(form.admissionNoticeAttachment)"
mode="aspectFill"
@click.stop="previewAttachment('admissionNoticeAttachment')"
/>
<view class="attachment-file-info">
<view class="attachment-name">{{ form.admissionNoticeAttachment.name }}</view>
<view class="attachment-type">{{ $tr('已上传图片,可点击查看大图') }}</view>
</view>
<view class="attachment-action-row">
<view class="attachment-action-btn" @click.stop="previewAttachment('admissionNoticeAttachment')">
{{ $tr('查看图片') }}
</view>
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('admissionNoticeAttachment')">
{{ $tr('重新上传') }}
</view>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">{{ $tr('杭州有无房产') }} <text class="required-mark">*</text></view>
<view class="tn-flex tn-flex-wrap">
<view class="tag-item" :class="form.hasHangzhouProperty === true ? 'tag-active' : ''" @click="form.hasHangzhouProperty = true">{{ $tr('有') }}</view>
<view class="tag-item" :class="form.hasHangzhouProperty === false ? 'tag-active' : ''" @click="form.hasHangzhouProperty = false">{{ $tr('无') }}</view>
</view>
</view>
<view class="form-item">
<view class="form-label">{{ $tr('证件类型') }} <text class="required-mark">*</text></view>
<view v-if="isDomesticIdentity" class="form-input picker-input picker-input-disabled">
<text class="text-black">{{ selectedIdCardTypeLabel || (language === 'en' ? 'ID Card' : '身份证') }}</text>
<text class="picker-fixed-text">{{ $tr('中国籍固定') }}</text>
</view>
<picker v-else mode="selector" :range="idCardTypeRange" @change="onIdCardTypeChange">
<view class="form-input picker-input">
<text :class="selectedIdCardTypeLabel ? 'text-black' : 'text-gray'">{{ selectedIdCardTypeLabel || $tr('请选择证件类型') }}</text>
<text class="tn-icon-right picker-arrow"></text>
</view>
</picker>
</view>
<template v-if="form.identityType === 'DomesticStudents'">
<view class="form-item">
<view class="form-label">{{ $tr('身份证号') }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="$tr('请输入身份证号')" placeholder-class="input-placeholder" v-model="form.idCard" />
</view>
<view class="form-item">
<view class="form-label">{{ $tr('身份证正面附件') }} <text class="required-mark">*</text></view>
<view v-if="!form.idCardFrontAttachment.serverValue" class="upload-box" @click="uploadAttachment('idCardFrontAttachment')">
<view class="upload-placeholder">
<view class="upload-icon-wrap"><text class="tn-icon-camera-fill"></text></view>
<view class="upload-text">{{ $tr('点击上传身份证正面') }}</view>
<view class="upload-subtext">{{ $tr('请保持边缘完整、无遮挡') }}</view>
</view>
</view>
<view v-else class="attachment-file-card">
<image
class="attachment-preview-image"
:src="getAttachmentPreviewUrl(form.idCardFrontAttachment)"
mode="aspectFill"
@click.stop="previewAttachment('idCardFrontAttachment')"
/>
<view class="attachment-file-info">
<view class="attachment-name">{{ form.idCardFrontAttachment.name }}</view>
<view class="attachment-type">{{ $tr('已上传图片,可点击查看大图') }}</view>
</view>
<view class="attachment-action-row">
<view class="attachment-action-btn" @click.stop="previewAttachment('idCardFrontAttachment')">
{{ $tr('查看图片') }}
</view>
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('idCardFrontAttachment')">
{{ $tr('重新上传') }}
</view>
</view>
</view>
</view>
<view class="form-item" style="margin-bottom: 0;">
<view class="form-label">{{ $tr('身份证反面附件') }} <text class="required-mark">*</text></view>
<view v-if="!form.idCardBackAttachment.serverValue" class="upload-box" @click="uploadAttachment('idCardBackAttachment')">
<view class="upload-placeholder">
<view class="upload-icon-wrap"><text class="tn-icon-camera-fill"></text></view>
<view class="upload-text">{{ $tr('点击上传身份证反面') }}</view>
<view class="upload-subtext">{{ $tr('请确保有效期和国徽信息清晰') }}</view>
</view>
</view>
<view v-else class="attachment-file-card">
<image
class="attachment-preview-image"
:src="getAttachmentPreviewUrl(form.idCardBackAttachment)"
mode="aspectFill"
@click.stop="previewAttachment('idCardBackAttachment')"
/>
<view class="attachment-file-info">
<view class="attachment-name">{{ form.idCardBackAttachment.name }}</view>
<view class="attachment-type">{{ $tr('已上传图片,可点击查看大图') }}</view>
</view>
<view class="attachment-action-row">
<view class="attachment-action-btn" @click.stop="previewAttachment('idCardBackAttachment')">
{{ $tr('查看图片') }}
</view>
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('idCardBackAttachment')">
{{ $tr('重新上传') }}
</view>
</view>
</view>
</view>
</template>
<template v-else>
<view class="form-item">
<view class="form-label">{{ foreignDocumentNumberLabel }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="foreignDocumentNumberPlaceholder" placeholder-class="input-placeholder" v-model="form.passportNumber" />
</view>
<view class="form-item">
<view class="form-label">{{ foreignDocumentExpiryLabel }} <text class="required-mark">*</text></view>
<picker mode="date" @change="onPassportDateChange">
<view class="form-input picker-input">
<text :class="form.passportExpiry ? 'text-black' : 'text-gray'">{{ form.passportExpiry || $tr('请选择日期') }}</text>
<text class="tn-icon-calendar picker-arrow"></text>
</view>
</picker>
</view>
<view class="form-item">
<view class="form-label">{{ $tr('就读学校') }}</view>
<input class="form-input" :placeholder="$tr('请输入就读学校(选填)')" placeholder-class="input-placeholder" v-model="form.school" />
</view>
<view class="form-item" style="margin-bottom: 0;">
<view class="form-label">{{ foreignDocumentAttachmentLabel }} <text class="required-mark">*</text></view>
<view v-if="!form.passportAttachment.serverValue" class="upload-box" @click="uploadAttachment('passportAttachment')">
<view class="upload-placeholder">
<view class="upload-icon-wrap"><text class="tn-icon-upload"></text></view>
<view class="upload-text">{{ foreignDocumentUploadText }}</view>
<view class="upload-subtext">{{ $tr('优先上传正面完整清晰的图片') }}</view>
</view>
</view>
<view v-else class="attachment-file-card">
<image
v-if="isImageAttachment(form.passportAttachment)"
class="attachment-preview-image"
:src="getAttachmentPreviewUrl(form.passportAttachment)"
mode="aspectFill"
@click.stop="previewAttachment('passportAttachment')"
/>
<view v-else class="attachment-file-cover is-unsupported">
<text class="attachment-file-ext">FILE</text>
</view>
<view class="attachment-file-info">
<view class="attachment-name">{{ form.passportAttachment.name }}</view>
<view class="attachment-type">
{{ isImageAttachment(form.passportAttachment) ? $tr('已上传图片,可点击查看大图') : foreignDocumentImageOnlyTip }}
</view>
</view>
<view class="attachment-action-row">
<view
v-if="isImageAttachment(form.passportAttachment)"
class="attachment-action-btn"
@click.stop="previewAttachment('passportAttachment')"
>
{{ $tr('查看图片') }}
</view>
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('passportAttachment')">
{{ $tr('重新上传') }}
</view>
</view>
</view>
</view>
</template>
</view>
</view>
<view class="form-card animation-fade-in" style="animation-delay: 0.36s;">
<view class="section-head">
<view class="section-index">04</view>
<view class="section-copy">
<view class="section-title">{{ $tr('偏好信息') }}</view>
<view class="section-desc">{{ $tr('选填项,用于更好地匹配入住生活偏好') }}</view>
</view>
</view>
<view class="section-body">
<view class="form-item">
<view class="form-label">{{ $tr('饮食习惯') }}</view>
<picker mode="selector" :range="dietaryHabitRange" @change="onDictSelect('dietaryHabit', $event)">
<view class="form-input picker-input">
<text :class="form.dietaryHabit ? 'text-black' : 'text-gray'">{{ form.dietaryHabit || $tr('请选择饮食习惯') }}</text>
<text class="tn-icon-right picker-arrow"></text>
</view>
</picker>
</view>
<view class="form-item">
<view class="form-label">{{ $tr('兴趣爱好') }}</view>
<picker mode="selector" :range="hobbyRange" @change="onDictSelect('hobby', $event)">
<view class="form-input picker-input">
<text :class="form.hobby ? 'text-black' : 'text-gray'">{{ form.hobby || $tr('请选择兴趣爱好') }}</text>
<text class="tn-icon-right picker-arrow"></text>
</view>
</picker>
</view>
<view class="form-item" style="margin-bottom: 0;">
<view class="form-label">{{ $tr('宗教信仰') }}</view>
<picker mode="selector" :range="religionRange" @change="onDictSelect('religion', $event)">
<view class="form-input picker-input">
<text :class="form.religion ? 'text-black' : 'text-gray'">{{ form.religion || $tr('请选择宗教信仰') }}</text>
<text class="tn-icon-right picker-arrow"></text>
</view>
</picker>
</view>
</view>
</view>
</view>
<view class="bottom-btn-area animation-fade-in" style="animation-delay: 0.44s;">
<view class="action-dock">
<view class="custom-btn btn-primary submit-btn" @click="submitForm" :class="{ 'btn-disabled': isSubmitting }">
<text class="submit-btn-text">{{ isSubmitting ? $tr('提交中...') : $tr('提交注册申请') }}</text>
</view>
<view class="secondary-entry-card" @click="goToRegistrationRecords">
<view class="secondary-entry-copy">
<view class="secondary-entry-title">{{ $tr('查看注册记录') }}</view>
<view class="secondary-entry-desc">{{ $tr('如果当前微信账号已提交过申请,可直接查看历史记录和审批状态') }}</view>
</view>
<text class="tn-icon-right secondary-entry-arrow"></text>
</view>
<view class="login-row">
<view class="login-text">{{ $tr('已有账号?') }}</view>
<view class="login-link" @click="goToLogin">{{ $tr('去登录') }}</view>
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<view v-if="showPrivacyPopup" class="privacy-mask">
<view class="privacy-dialog">
<view class="privacy-title">{{ $tr('隐私授权提示') }}</view>
<view class="privacy-content">
{{ $tr('上传证件图片前,请先阅读并同意') }}
<text class="privacy-link" @click="openPrivacyContract">{{ $tr('《用户隐私保护指引》') }}</text>
{{ $tr(',同意后才能继续选择图片。') }}
</view>
<view class="privacy-action-row">
<view class="privacy-action-btn" @click="handlePrivacyDisagree">{{ $tr('暂不授权') }}</view>
<button
id="register-privacy-agree-btn"
class="privacy-action-btn is-primary"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
>
{{ $tr('同意并继续') }}
</button>
</view>
</view>
</view>
<!-- #endif -->
</view>
</template>
<script>
import {
clearFailedRegistrationDraft,
clearLoginSession,
getFailedRegistrationDraft,
saveFailedRegistrationDraft
} from '../../utils/app-state'
import { getCountryList, submitRegistrationApplication, sysdictPage, uploadCommonFile, registrationMyorder } from '@/api/index.js'
import { addFilePrefix } from '@/utils/common.js'
import { bindPrivacyAuthorizationListener, openWechatPrivacyContract, unbindPrivacyAuthorizationListener } from '@/utils/privacy.js'
import { fetchEnums, getEnumOptions, getGenderLabel } from '../../utils/enum-store'
import { resolveRegistrationApplicationCode } from '../../utils/flow'
function createAttachmentState() {
return {
name: '',
serverValue: '',
localPath: ''
}
}
function createDefaultForm() {
return {
identityType: 'ForeignStudents',
account: '',
password: '',
email: '',
emailVerifyCode: '',
name: '',
gender: 1,
phone: '',
country: '',
idCardType: 2,
idCard: '',
workUnit: '',
hasHangzhouProperty: null,
dietaryHabit: '',
hobby: '',
passportNumber: '',
passportExpiry: '',
school: '',
religion: '',
admissionNoticeAttachment: createAttachmentState(),
passportAttachment: createAttachmentState(),
idCardFrontAttachment: createAttachmentState(),
idCardBackAttachment: createAttachmentState()
}
}
function normalizeAttachmentDraft(attachment) {
const source = attachment && typeof attachment === 'object' ? attachment : {}
return {
...createAttachmentState(),
name: source.name || '',
serverValue: source.serverValue || '',
localPath: source.localPath || ''
}
}
function normalizeRegistrationDraftForm(draft) {
const draftSource = draft && typeof draft === 'object' ? draft : {}
const source = draftSource.form && typeof draftSource.form === 'object' ? draftSource.form : draftSource
return {
...createDefaultForm(),
...source,
gender: source.gender === 0 || source.gender === 1 ? Number(source.gender) : 1,
idCardType: Number(source.idCardType || source.IdCardType || 0) || 2,
hasHangzhouProperty: typeof source.hasHangzhouProperty === 'boolean' ? source.hasHangzhouProperty : null,
admissionNoticeAttachment: normalizeAttachmentDraft(source.admissionNoticeAttachment),
passportAttachment: normalizeAttachmentDraft(source.passportAttachment),
idCardFrontAttachment: normalizeAttachmentDraft(source.idCardFrontAttachment),
idCardBackAttachment: normalizeAttachmentDraft(source.idCardBackAttachment)
}
}
export default {
data() {
return {
navBgOpacity: 0,
isSubmitting: false,
dictLoading: false,
hasPromptedFailedDraft: false,
draftPersistTimer: null,
draftTipText: '',
lastForeignCountry: '',
lastForeignIdCardType: 2,
showPrivacyPopup: false,
resolvePrivacyAuthorization: null,
form: createDefaultForm(),
rawCountryList: [],
rawHobbyList: [],
rawDietaryHabitList: [],
rawReligionList: [],
rawIdCardTypeList: [],
countryOptions: [],
hobbyOptions: [],
dietaryHabitOptions: [],
religionOptions: [],
idCardTypeOptions: []
}
},
computed: {
language() {
return this.$getLanguage()
},
navBackgroundColor() {
return `rgba(251, 247, 241, ${this.navBgOpacity})`
},
hobbyRange() {
return this.hobbyOptions.length ? this.hobbyOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
countryRange() {
return this.countryOptions.length ? this.countryOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
idCardTypeRange() {
return this.idCardTypeOptions.length ? this.idCardTypeOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
dietaryHabitRange() {
return this.dietaryHabitOptions.length ? this.dietaryHabitOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
isDomesticIdentity() {
return this.form.identityType === 'DomesticStudents'
},
religionRange() {
return this.religionOptions.length ? this.religionOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
selectedIdCardTypeLabel() {
const matched = this.idCardTypeOptions.find(item => String(item.key) === String(this.form.idCardType))
return matched ? matched.value : ''
},
isPassportType() {
return String(this.form.idCardType) === '2'
},
foreignDocumentNumberLabel() {
return this.isPassportType ? this.$tr('护照号') : this.$tr('证件号')
},
foreignDocumentNumberPlaceholder() {
return this.isPassportType ? this.$tr('请输入护照号') : this.$tr('请输入证件号')
},
foreignDocumentExpiryLabel() {
return this.isPassportType ? this.$tr('护照到期时间') : this.$tr('证件到期时间')
},
foreignDocumentAttachmentLabel() {
return this.isPassportType ? this.$tr('护照附件') : this.$tr('证件附件')
},
foreignDocumentUploadText() {
return this.isPassportType ? this.$tr('点击上传护照图片') : this.$tr('点击上传证件图片')
},
foreignDocumentImageOnlyTip() {
return this.isPassportType ? this.$tr('护照附件仅支持图片,请重新上传') : this.$tr('证件附件仅支持图片,请重新上传')
},
genderOptions() {
return [
{ key: 1, value: this.$tr('男') },
{ key: 0, value: this.$tr('女') }
]
}
},
watch: {
language() {
this.rebuildLocalizedOptions()
this.draftTipText = this.$tr('未完成内容会自动保存,可随时返回继续填写')
},
form: {
deep: true,
handler() {
this.scheduleDraftPersist()
}
}
},
async onLoad(options) {
this.draftTipText = this.$tr('未完成内容会自动保存,可随时返回继续填写')
this.initPrivacyAuthorization()
const bypass = options && options.bypass === '1'
const hasRedirected = await this.checkPendingRegistration(bypass)
if (hasRedirected) return
await Promise.all([
this.loadDictOptions(),
this.loadCountryOptions()
])
this.tryRestoreFailedDraft()
},
onHide() {
this.persistRegistrationDraft()
},
onUnload() {
this.persistRegistrationDraft()
this.clearDraftPersistTimer()
this.resolvePrivacyAuthorization = null
this.releasePrivacyAuthorization()
},
onPageScroll(e) {
let opacity = e.scrollTop / 80
if (opacity > 1) {
opacity = 1
} else if (opacity < 0) {
opacity = 0
}
this.navBgOpacity = opacity
},
methods: {
isChinaCountry(value = '') {
const text = String(value || '').trim()
return text === '中国' || text === 'China' || text === this.$tr('中国')
},
async checkPendingRegistration(bypassRedirect = false) {
uni.showLoading({ title: this.$tr('加载中...') })
try {
const providerValue = await this.getWechatCode()
if (!providerValue) return false
const response = await registrationMyorder({ providerValue }, this.language)
const rawLatest = response && response.data && typeof response.data === 'object' ? response.data : response
const latest = rawLatest.data !== undefined ? rawLatest.data : rawLatest
if (latest && typeof latest === 'object' && Object.keys(latest).length > 0) {
if (!bypassRedirect) {
uni.redirectTo({ url: '/pkgAccount/auth_result/auth_result' })
return true
}
}
} catch (e) {
console.error('check pending registration failed', e)
} finally {
uni.hideLoading()
}
return false
},
goToLogin() {
uni.navigateBack()
},
goToRegistrationRecords() {
uni.navigateTo({
url: '/pkgAccount/registration_records/registration_records'
})
},
tryRestoreFailedDraft() {
if (this.hasPromptedFailedDraft) {
return
}
const cachedDraft = getFailedRegistrationDraft()
if (!cachedDraft || typeof cachedDraft !== 'object') {
return
}
this.hasPromptedFailedDraft = true
uni.showModal({
title: this.$tr('发现未完成的数据'),
content: this.$tr('是否填入上次未完成或提交失败时缓存的注册信息?'),
confirmText: this.$tr('填入'),
cancelText: this.$tr('不用'),
success: ({ confirm }) => {
if (confirm) {
this.applyFailedDraft(cachedDraft)
return
}
clearFailedRegistrationDraft()
}
})
},
applyFailedDraft(draft) {
this.form = normalizeRegistrationDraftForm(draft)
if (!this.isDomesticIdentity && this.form.country && !this.isChinaCountry(this.form.country)) {
this.lastForeignCountry = this.form.country
}
if (!this.isDomesticIdentity && String(this.form.idCardType || '') !== '1') {
this.lastForeignIdCardType = Number(this.form.idCardType) || 2
}
this.applyIdentityDocumentTypeRule()
this.applyIdentityCountryRule()
uni.showToast({
title: this.$tr('已填入上次失败数据'),
icon: 'success'
})
},
buildFailedDraft() {
return normalizeRegistrationDraftForm(this.form)
},
scheduleDraftPersist() {
if (this.isSubmitting) {
return
}
this.clearDraftPersistTimer()
this.draftPersistTimer = setTimeout(() => {
this.persistRegistrationDraft(true)
}, 600)
},
clearDraftPersistTimer() {
if (!this.draftPersistTimer) {
return
}
clearTimeout(this.draftPersistTimer)
this.draftPersistTimer = null
},
hasDraftContent() {
const attachmentFields = [
'admissionNoticeAttachment',
'passportAttachment',
'idCardFrontAttachment',
'idCardBackAttachment'
]
const textFields = [
'account',
'password',
'email',
'emailVerifyCode',
'name',
'phone',
'country',
'idCard',
'workUnit',
'dietaryHabit',
'hobby',
'passportNumber',
'passportExpiry',
'school',
'religion'
]
if (this.form.identityType !== 'ForeignStudents') {
return true
}
if (this.form.gender !== null || this.form.hasHangzhouProperty !== null) {
return true
}
if (textFields.some(field => String(this.form[field] || '').trim())) {
return true
}
return attachmentFields.some((field) => {
const attachment = this.form[field]
return attachment && (attachment.serverValue || attachment.localPath || attachment.name)
})
},
persistRegistrationDraft(isSilent = false) {
if (this.isSubmitting) {
return
}
this.clearDraftPersistTimer()
if (!this.hasDraftContent()) {
clearFailedRegistrationDraft()
this.draftTipText = this.$tr('未完成内容会自动保存,可随时返回继续填写')
return
}
saveFailedRegistrationDraft({
form: this.buildFailedDraft(),
updatedAt: Date.now()
})
if (!isSilent) {
this.draftTipText = this.$tr('当前填写内容已自动保存')
}
},
switchIdentityType(type) {
if (this.form.identityType === type) return
if (type === 'DomesticStudents' && this.form.country) {
this.lastForeignCountry = this.form.country
}
this.form.identityType = type
this.applyIdentityDocumentTypeRule()
this.applyIdentityCountryRule()
},
onPassportDateChange(e) {
this.form.passportExpiry = e.detail.value
},
loadCountryOptions() {
return getCountryList(this.language)
.then((countryRes) => {
const rawList = this.extractResponseList(countryRes)
this.rawCountryList = rawList
this.countryOptions = this.normalizeCountryList(rawList)
this.applyIdentityCountryRule()
})
.catch((error) => {
console.error('load country options failed', error)
uni.showToast({
title: this.$tr('国家列表加载失败'),
icon: 'none'
})
})
},
loadDictOptions() {
this.dictLoading = true
return Promise.all([
sysdictPage({ Type: 'UserHobbiesAndInterestsTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
sysdictPage({ Type: 'UserDietaryHabitsTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
sysdictPage({ Type: 'UserReligiousBeliefTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
fetchEnums(false, this.$getLanguage())
])
.then(([hobbyRes, dietaryRes, religionRes]) => {
this.rawHobbyList = this.extractResponseList(hobbyRes)
this.rawDietaryHabitList = this.extractResponseList(dietaryRes)
this.rawReligionList = this.extractResponseList(religionRes)
this.rawIdCardTypeList = getEnumOptions('IdCardTypeEnum', this.$getLanguage())
this.hobbyOptions = this.normalizeDictList(this.rawHobbyList)
this.dietaryHabitOptions = this.normalizeDictList(this.rawDietaryHabitList)
this.religionOptions = this.normalizeDictList(this.rawReligionList)
this.idCardTypeOptions = this.normalizeDictList(this.rawIdCardTypeList)
this.applyIdentityDocumentTypeRule()
})
.catch((error) => {
console.error('load dict options failed', error)
uni.showToast({
title: this.$tr('字典加载失败,稍后可重试'),
icon: 'none'
})
})
.finally(() => {
this.dictLoading = false
})
},
extractResponseList(response) {
const rawList = response && response.data
? (response.data.data || response.data)
: (response && response.data ? response.data : response)
return Array.isArray(rawList) ? rawList : []
},
getOptionTextCandidates(item = {}) {
return [
item.value,
item.label,
item.description,
item.name,
item.cname,
item.nameZh,
item.labelZh,
item.valueZh,
item.descriptionZh,
item.ename,
item.nameEn,
item.labelEn,
item.valueEn,
item.descriptionEn,
item.code
].map(value => String(value || '').trim()).filter(Boolean)
},
getLocalizedOptionText(item = {}) {
const isEn = this.language === 'en'
const preferred = isEn
? [item.descriptionEn, item.ename, item.nameEn, item.labelEn, item.valueEn, item.description, item.value, item.label, item.name, item.cname, item.code]
: [item.description, item.cname, item.nameZh, item.labelZh, item.valueZh, item.value, item.label, item.name, item.code]
const matched = preferred.map(value => String(value || '').trim()).find(Boolean)
return matched || ''
},
normalizeDictList(rawList = []) {
const list = Array.isArray(rawList) ? rawList : []
return list
.map(item => ({
key: item.key || item.code || '',
value: this.getLocalizedOptionText(item)
}))
.filter(item => item.value)
},
normalizeCountryList(rawList = []) {
const list = Array.isArray(rawList) ? rawList : []
return list
.map(item => ({
key: item.code || item.key || item.cname || '',
value: this.getLocalizedOptionText(item)
}))
.filter(item => item.value)
},
remapSelectedValue(currentValue, rawList = []) {
const normalizedCurrent = String(currentValue || '').trim()
if (!normalizedCurrent) {
return ''
}
const matched = (Array.isArray(rawList) ? rawList : []).find((item) => {
return this.getOptionTextCandidates(item).includes(normalizedCurrent)
})
return matched ? this.getLocalizedOptionText(matched) : normalizedCurrent
},
rebuildLocalizedOptions() {
this.countryOptions = this.normalizeCountryList(this.rawCountryList)
this.hobbyOptions = this.normalizeDictList(this.rawHobbyList)
this.dietaryHabitOptions = this.normalizeDictList(this.rawDietaryHabitList)
this.religionOptions = this.normalizeDictList(this.rawReligionList)
this.idCardTypeOptions = this.normalizeDictList(this.rawIdCardTypeList)
this.form.country = this.remapSelectedValue(this.form.country, this.rawCountryList)
this.lastForeignCountry = this.remapSelectedValue(this.lastForeignCountry, this.rawCountryList)
this.form.hobby = this.remapSelectedValue(this.form.hobby, this.rawHobbyList)
this.form.dietaryHabit = this.remapSelectedValue(this.form.dietaryHabit, this.rawDietaryHabitList)
this.form.religion = this.remapSelectedValue(this.form.religion, this.rawReligionList)
this.applyIdentityDocumentTypeRule()
this.applyIdentityCountryRule()
},
applyIdentityDocumentTypeRule() {
if (this.isDomesticIdentity) {
if (String(this.form.idCardType || '') !== '1' && String(this.form.idCardType || '')) {
this.lastForeignIdCardType = Number(this.form.idCardType) || 2
}
this.form.idCardType = 1
return
}
if (!this.form.idCardType || String(this.form.idCardType) === '1') {
this.form.idCardType = this.lastForeignIdCardType || 2
}
},
getChinaCountryOption() {
return this.countryOptions.find(item => String(item.key || '').toUpperCase() === 'CHN' || this.isChinaCountry(item.value)) || null
},
applyIdentityCountryRule() {
if (this.isDomesticIdentity) {
const chinaOption = this.getChinaCountryOption()
this.form.country = chinaOption ? chinaOption.value : this.$tr('中国')
return
}
if (this.isChinaCountry(this.form.country)) {
this.form.country = this.lastForeignCountry || ''
}
},
onDictSelect(field, event) {
const map = {
dietaryHabit: this.dietaryHabitOptions,
hobby: this.hobbyOptions,
religion: this.religionOptions
}
const options = map[field] || []
if (!options.length) {
uni.showToast({
title: this.$tr('当前暂无可选项'),
icon: 'none'
})
return
}
const selected = options[event.detail.value]
this.form[field] = selected ? selected.value : ''
},
onIdCardTypeChange(event) {
if (!this.idCardTypeOptions.length) {
uni.showToast({
title: this.$tr('当前暂无可选项'),
icon: 'none'
})
return
}
const selected = this.idCardTypeOptions[event.detail.value]
const nextValue = selected ? Number(selected.key) : 0
this.form.idCardType = nextValue || this.form.idCardType
if (!this.isDomesticIdentity && nextValue && nextValue !== 1) {
this.lastForeignIdCardType = nextValue
}
},
onCountryChange(event) {
if (this.isDomesticIdentity) return
if (!this.countryOptions.length) {
uni.showToast({
title: this.$tr('当前暂无可选国家'),
icon: 'none'
})
return
}
const selected = this.countryOptions[event.detail.value]
this.form.country = selected ? selected.value : ''
this.lastForeignCountry = this.form.country || ''
},
initPrivacyAuthorization() {
// #ifdef MP-WEIXIN
if (typeof wx === 'undefined' || !wx.onNeedPrivacyAuthorization) {
return
}
this.handlePrivacyAuthorizationRequest = (resolve) => {
this.resolvePrivacyAuthorization = resolve
this.showPrivacyPopup = true
}
bindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
// #endif
},
releasePrivacyAuthorization() {
// #ifdef MP-WEIXIN
if (!this.handlePrivacyAuthorizationRequest) {
return
}
unbindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
this.handlePrivacyAuthorizationRequest = null
this.showPrivacyPopup = false
// #endif
},
ensurePrivacyAuthorized() {
// #ifdef MP-WEIXIN
if (typeof wx === 'undefined' || !wx.requirePrivacyAuthorize) {
return Promise.resolve()
}
return new Promise((resolve, reject) => {
const triggerPrivacyAuthorize = () => {
wx.requirePrivacyAuthorize({
success: () => {
this.showPrivacyPopup = false
resolve()
},
fail: (error) => {
reject({
...error,
privacyDenied: true
})
}
})
}
if (!wx.getPrivacySetting) {
triggerPrivacyAuthorize()
return
}
wx.getPrivacySetting({
success: (res) => {
if (!res.needAuthorization) {
resolve()
return
}
triggerPrivacyAuthorize()
},
fail: () => {
triggerPrivacyAuthorize()
}
})
})
// #endif
// #ifndef MP-WEIXIN
return Promise.resolve()
// #endif
},
openPrivacyContract() {
// #ifdef MP-WEIXIN
openWechatPrivacyContract().catch((error) => {
console.error('open privacy contract failed', error)
})
// #endif
},
handleAgreePrivacyAuthorization() {
this.showPrivacyPopup = false
if (!this.resolvePrivacyAuthorization) {
return
}
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
this.resolvePrivacyAuthorization = null
resolvePrivacyAuthorization({
buttonId: 'register-privacy-agree-btn',
event: 'agree'
})
},
handlePrivacyDisagree() {
this.showPrivacyPopup = false
if (!this.resolvePrivacyAuthorization) {
return
}
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
this.resolvePrivacyAuthorization = null
resolvePrivacyAuthorization({
event: 'disagree'
})
},
uploadAttachment(field) {
return this.ensurePrivacyAuthorized()
.then(() => this.chooseImageFile())
.then((file) => {
if (!file || !file.path) return
uni.showLoading({
title: this.$tr('上传中...')
})
return uploadCommonFile(file.path).then((uploadResult) => {
const serverValue = this.resolveUploadedValue(uploadResult, file.path)
this.form[field] = {
name: file.name || this.getDefaultAttachmentName(field, file.path),
serverValue,
localPath: file.path
}
uni.showToast({
title: this.$tr('上传成功'),
icon: 'success'
})
})
})
.catch((error) => {
if (error && error.cancel) return
if (error && error.privacyDenied) {
uni.showToast({
title: this.$tr('未完成隐私授权,暂时无法选择图片'),
icon: 'none'
})
return
}
console.error('upload attachment failed', error)
uni.showToast({
title: this.$tr('上传失败,请重试'),
icon: 'none'
})
})
.finally(() => {
uni.hideLoading()
})
},
chooseImageFile() {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
success: (res) => {
const filePath = res.tempFilePaths && res.tempFilePaths[0]
const tempFile = res.tempFiles && res.tempFiles[0]
resolve({
path: filePath,
name: tempFile && tempFile.name ? tempFile.name : this.getFileNameFromPath(filePath)
})
},
fail: (error) => {
if (error && /cancel/i.test(JSON.stringify(error))) {
reject({ cancel: true })
return
}
reject(error)
}
})
})
},
previewAttachment(field) {
const attachment = this.form[field]
const previewUrl = this.getAttachmentPreviewUrl(attachment)
if (!previewUrl || !this.isImageAttachment(attachment)) {
uni.showToast({
title: this.$tr('当前附件不是图片'),
icon: 'none'
})
return
}
uni.previewImage({
urls: [previewUrl],
current: previewUrl
})
},
getAttachmentPreviewUrl(attachment) {
if (!attachment) return ''
const candidates = [attachment.serverValue, attachment.localPath]
for (const item of candidates) {
const value = String(item || '').trim()
if (!value) continue
if (this.isLocalFileUrl(value)) {
return value
}
return addFilePrefix(value)
}
return ''
},
isImageAttachment(attachment) {
return ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(this.getAttachmentExtension(attachment))
},
isLocalFileUrl(url = '') {
return /^(wxfile:\/\/|file:\/\/|blob:|data:image|_doc\/|\/tmp\/)/i.test(String(url || '').trim())
},
getAttachmentExtension(attachment) {
const candidates = [
attachment && attachment.name,
attachment && attachment.serverValue,
attachment && attachment.localPath
]
const matched = candidates.find(item => String(item || '').trim())
if (!matched) return ''
const cleanValue = String(matched).split('?')[0].split('#')[0]
const segments = cleanValue.split('.')
return segments.length > 1 ? segments.pop().toLowerCase() : ''
},
submitForm() {
if (this.isSubmitting) return
const errorMessage = this.validateForm()
if (errorMessage) {
uni.showToast({
title: errorMessage,
icon: 'none'
})
return
}
this.isSubmitting = true
uni.showLoading({
title: this.$tr('提交中...')
})
return this.getWechatCode().then((providerValue) => {
const submitPayload = this.buildSubmitPayload(providerValue)
return submitRegistrationApplication(submitPayload)
.then((response) => {
const registrationCode = resolveRegistrationApplicationCode(response)
clearLoginSession()
clearFailedRegistrationDraft()
uni.hideLoading()
uni.showToast({
title: this.$tr('申请已提交,待审批'),
icon: 'success'
})
setTimeout(() => {
uni.redirectTo({
url: '/pkgAccount/auth_result/auth_result'
})
}, 800)
})
.catch((error) => {
console.error('submit registration application failed', error)
saveFailedRegistrationDraft(this.buildFailedDraft())
uni.hideLoading()
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('提交失败,请稍后重试'),
icon: 'none'
})
})
.finally(() => {
this.isSubmitting = false
})
})
},
getWechatCode() {
return new Promise((resolve) => {
// #ifdef MP-WEIXIN
uni.login({
provider: 'weixin',
success: (res) => {
resolve(res.code || '')
},
fail: (err) => {
console.error('getWechatCode failed', err)
resolve('')
}
})
// #endif
// #ifndef MP-WEIXIN
resolve('')
// #endif
})
},
resolveUploadedValue(uploadResult, fallback) {
const candidates = [
uploadResult && uploadResult.data && uploadResult.data.url,
uploadResult && uploadResult.data && uploadResult.data.fileUrl,
uploadResult && uploadResult.data && uploadResult.data.path,
uploadResult && uploadResult.data && uploadResult.data.filePath,
uploadResult && uploadResult.url,
uploadResult && uploadResult.fileUrl,
uploadResult && uploadResult.path,
uploadResult && uploadResult.filePath,
typeof (uploadResult && uploadResult.data) === 'string' ? uploadResult.data : '',
typeof uploadResult === 'string' ? uploadResult : ''
]
return candidates.find(item => typeof item === 'string' && item) || fallback
},
getDefaultAttachmentName(field, filePath) {
if (field === 'passportAttachment') return 'passport-attachment'
if (field === 'idCardFrontAttachment') return 'id-card-front'
if (field === 'idCardBackAttachment') return 'id-card-back'
if (field === 'admissionNoticeAttachment') return 'admission-notice-attachment'
return this.getFileNameFromPath(filePath)
},
getFileNameFromPath(filePath = '') {
const normalizedPath = String(filePath || '').replace(/\\/g, '/')
return normalizedPath.split('/').pop() || 'attachment'
},
validateForm() {
if (!this.form.account || !this.form.password || !this.form.email || !this.form.name) {
return this.$tr('请先填写账号、密码、邮箱和姓名')
}
if (this.form.password.length < 6) {
return this.$tr('登录密码至少 6 位')
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.form.email)) {
return this.$tr('请输入正确的邮箱地址')
}
if (this.form.gender === null || !this.form.phone || !this.form.country) {
return this.$tr('请完善性别、手机号和国籍')
}
if (!/^1\d{10}$/.test(this.form.phone)) {
return this.$tr('请输入正确的手机号')
}
if (!this.form.workUnit || this.form.hasHangzhouProperty === null) {
return this.$tr('请完善工作单位和杭州房产情况')
}
if (!this.form.idCardType) {
return this.$tr('请选择证件类型')
}
if (this.form.identityType === 'DomesticStudents') {
if (!this.form.idCard) {
return this.$tr('请填写身份证号')
}
if (!this.form.idCardFrontAttachment.serverValue || !this.form.idCardBackAttachment.serverValue) {
return this.$tr('请上传身份证正反面附件')
}
return ''
}
if (!this.form.passportNumber || !this.form.passportExpiry) {
return this.isPassportType ? this.$tr('请填写护照号和护照到期时间') : this.$tr('请填写证件号和证件到期时间')
}
if (!this.form.passportAttachment.serverValue) {
return this.isPassportType ? this.$tr('请上传护照图片') : this.$tr('请上传证件图片')
}
if (!this.isImageAttachment(this.form.passportAttachment)) {
return this.isPassportType ? this.$tr('护照附件仅支持图片格式') : this.$tr('证件附件仅支持图片格式')
}
return ''
},
buildSingleTag(text, options) {
if (!text) return []
const matched = (options || []).find(item => item.value === text)
if (matched) {
return [{ key: matched.key, value: matched.value }]
}
return [{ key: text, value: text }]
},
buildAuthProfile() {
return {
identityType: this.form.identityType,
account: this.form.account,
email: this.form.email,
emailVerifyCode: this.form.emailVerifyCode,
roleNo: this.form.identityType,
name: this.form.name,
gender: this.form.gender,
genderText: getGenderLabel(this.form.gender, this.form.gender === 1 ? this.$tr('男') : this.form.gender === 0 ? this.$tr('女') : ''),
phone: this.form.phone,
country: this.form.country,
idCardType: Number(this.form.idCardType) || null,
idCardTypeText: this.selectedIdCardTypeLabel || '',
idCard: this.form.identityType === 'DomesticStudents' ? this.form.idCard : '',
workUnit: this.form.workUnit,
hasHangzhouProperty: this.form.hasHangzhouProperty,
dietaryHabit: this.form.dietaryHabit,
dietaryHabitTags: this.buildSingleTag(this.form.dietaryHabit, this.dietaryHabitOptions),
hobby: this.form.hobby,
hobbyTags: this.buildSingleTag(this.form.hobby, this.hobbyOptions),
passportNumber: this.form.identityType === 'ForeignStudents' ? this.form.passportNumber : '',
passportExpiry: this.form.identityType === 'ForeignStudents' && this.form.passportExpiry
? `${this.form.passportExpiry}T00:00:00`
: '',
school: this.form.identityType === 'ForeignStudents' ? this.form.school : '',
religion: this.form.religion,
religionTags: this.buildSingleTag(this.form.religion, this.religionOptions),
admissionNoticeAttachment: this.form.admissionNoticeAttachment.serverValue || '',
passportAttachment: this.form.identityType === 'ForeignStudents' ? this.form.passportAttachment.serverValue : '',
idCardFrontAttachment: this.form.identityType === 'DomesticStudents' ? this.form.idCardFrontAttachment.serverValue : '',
idCardBackAttachment: this.form.identityType === 'DomesticStudents' ? this.form.idCardBackAttachment.serverValue : ''
}
},
buildSubmitPayload(providerValue) {
const profile = this.buildAuthProfile()
return {
providerValue: providerValue || null,
account: profile.account,
password: this.form.password,
email: profile.email,
emailVerifyCode: profile.emailVerifyCode || null,
roleNo: profile.roleNo,
name: profile.name,
gender: profile.gender,
phone: profile.phone,
country: profile.country,
idCardType: profile.idCardType || null,
IdCardType: profile.idCardType || null,
idCard: profile.idCard || null,
workUnit: profile.workUnit,
hasHangzhouProperty: profile.hasHangzhouProperty,
dietaryHabitTags: profile.dietaryHabitTags.length ? profile.dietaryHabitTags : null,
hobbyTags: profile.hobbyTags.length ? profile.hobbyTags : null,
passportNumber: profile.passportNumber || null,
passportExpiry: profile.passportExpiry || null,
school: profile.school || null,
religionTags: profile.religionTags.length ? profile.religionTags : null,
admissionNoticeAttachment: profile.admissionNoticeAttachment || null,
passportAttachment: profile.passportAttachment || null,
idCardFrontAttachment: profile.idCardFrontAttachment || null,
idCardBackAttachment: profile.idCardBackAttachment || null
}
},
}
}
</script>
<style scoped lang="scss">
$brand: #2B8EFF;
$brand-soft: rgba(43, 142, 255, 0.12);
$brand-ink: #0F3F78;
$bg-base: #F4F7FC;
$surface: rgba(255, 255, 255, 0.82);
$surface-solid: #FFFFFF;
$text-main: #122033;
$text-sub: #708198;
$text-light: #94A3B8;
$line-soft: rgba(43, 142, 255, 0.1);
$shadow-soft: 0 20rpx 60rpx rgba(64, 108, 170, 0.08);
.register-page {
min-height: 100vh;
position: relative;
background:
linear-gradient(180deg, #F9FBFF 0%, #F4F7FC 36%, #EEF3FB 100%);
overflow: hidden;
}
.register-page::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at top left, rgba(108, 176, 255, 0.18) 0, rgba(108, 176, 255, 0) 34%),
radial-gradient(circle at 85% 12%, rgba(153, 210, 255, 0.16) 0, rgba(153, 210, 255, 0) 26%);
pointer-events: none;
}
.nav-title {
font-size: 32rpx;
font-weight: 600;
letter-spacing: 1rpx;
color: $text-main;
}
.register-content {
position: relative;
z-index: 1;
padding: 0 28rpx 360rpx;
}
.ambient-orb {
position: absolute;
border-radius: 50%;
filter: blur(16rpx);
pointer-events: none;
}
.orb-a {
width: 280rpx;
height: 280rpx;
top: 90rpx;
right: -80rpx;
background: rgba(43, 142, 255, 0.16);
}
.orb-b {
width: 220rpx;
height: 220rpx;
top: 520rpx;
left: -100rpx;
background: rgba(157, 213, 255, 0.18);
}
.ambient-grid {
position: absolute;
top: 140rpx;
right: 20rpx;
width: 200rpx;
height: 220rpx;
opacity: 0.24;
background-image:
linear-gradient(rgba(43, 142, 255, 0.16) 1px, transparent 1px),
linear-gradient(90deg, rgba(43, 142, 255, 0.16) 1px, transparent 1px);
background-size: 26rpx 26rpx;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 1), transparent);
pointer-events: none;
}
.page-hero {
position: relative;
margin-top: 30rpx;
padding: 44rpx 32rpx 36rpx;
border-radius: 40rpx;
background:
linear-gradient(140deg, rgba(255, 255, 255, 0.94) 0%, rgba(247, 251, 255, 0.78) 100%);
border: 1px solid rgba(255, 255, 255, 0.72);
box-shadow: 0 28rpx 80rpx rgba(90, 132, 190, 0.12);
backdrop-filter: blur(22rpx);
}
.hero-kicker {
display: inline-flex;
align-items: center;
height: 52rpx;
padding: 0 22rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(43, 142, 255, 0.12);
color: $brand-ink;
font-size: 22rpx;
letter-spacing: 2rpx;
}
.page-title {
margin-top: 28rpx;
font-size: 60rpx;
line-height: 1.16;
font-weight: 600;
letter-spacing: 1rpx;
color: $text-main;
}
.page-desc {
margin-top: 18rpx;
font-size: 26rpx;
line-height: 1.72;
color: $text-sub;
}
.draft-tip-bar {
display: inline-flex;
align-items: center;
margin-top: 26rpx;
padding: 14rpx 24rpx;
border-radius: 999rpx;
background: rgba(43, 142, 255, 0.08);
border: 1px solid rgba(43, 142, 255, 0.1);
}
.draft-tip-icon {
margin-right: 12rpx;
font-size: 24rpx;
color: $brand;
}
.draft-tip-text {
font-size: 22rpx;
font-weight: 500;
color: $brand-ink;
}
.hero-badge-list {
display: flex;
flex-wrap: wrap;
margin: 28rpx -9rpx 0;
}
.hero-badge-card {
min-width: calc((100% - 36rpx) / 3);
flex: 1;
margin: 0 9rpx 18rpx;
padding: 22rpx 22rpx 20rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
box-sizing: border-box;
}
.hero-badge-label {
font-size: 22rpx;
color: $text-light;
}
.hero-badge-value {
margin-top: 10rpx;
font-size: 28rpx;
font-weight: 600;
color: $text-main;
}
.progress-strip {
display: flex;
align-items: center;
padding: 14rpx;
margin: 28rpx 0 22rpx;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.62);
border: 1px solid rgba(255, 255, 255, 0.78);
box-shadow: $shadow-soft;
backdrop-filter: blur(18rpx);
}
.progress-node {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 18rpx 8rpx;
border-radius: 24rpx;
color: $text-sub;
}
.progress-node.is-active {
background: rgba(43, 142, 255, 0.1);
color: $brand-ink;
}
.progress-node-index {
margin-bottom: 8rpx;
font-size: 20rpx;
font-weight: 600;
letter-spacing: 2rpx;
}
.progress-node-text {
font-size: 24rpx;
font-weight: 500;
}
.form-card {
position: relative;
margin-bottom: 28rpx;
padding: 34rpx 26rpx 26rpx;
border-radius: 36rpx;
background: $surface;
border: 1px solid rgba(255, 255, 255, 0.72);
box-shadow: $shadow-soft;
backdrop-filter: blur(18rpx);
}
.section-head {
display: flex;
align-items: center;
margin-bottom: 26rpx;
}
.section-index {
margin-right: 22rpx;
width: 88rpx;
height: 88rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, rgba(43, 142, 255, 0.16) 0%, rgba(43, 142, 255, 0.08) 100%);
color: $brand-ink;
font-size: 24rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.section-copy {
flex: 1;
min-width: 0;
}
.section-title {
font-size: 34rpx;
font-weight: 600;
color: $text-main;
}
.section-desc {
margin-top: 10rpx;
font-size: 24rpx;
line-height: 1.6;
color: $text-sub;
}
.section-body {
display: flex;
flex-direction: column;
}
.section-body > .identity-panel + .form-item,
.section-body > .form-item + .form-item,
.section-body > .form-item + .identity-panel,
.section-body > .identity-panel + .identity-panel {
margin-top: 18rpx;
}
.identity-panel,
.form-item {
padding: 26rpx 24rpx 24rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.74);
border: 1px solid rgba(43, 142, 255, 0.08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
margin-bottom: 0;
}
.identity-panel-label {
font-size: 24rpx;
font-weight: 500;
color: $text-light;
}
.identity-panel-tip {
margin-top: 16rpx;
font-size: 22rpx;
line-height: 1.6;
color: $text-sub;
}
.form-label {
margin-bottom: 16rpx;
font-size: 25rpx;
font-weight: 600;
color: $text-main;
}
.required-mark {
margin-left: 6rpx;
color: #F06E53;
}
.form-input {
height: 96rpx;
padding: 0 28rpx;
border-radius: 24rpx;
box-sizing: border-box;
background: $surface-solid;
border: 1px solid $line-soft;
color: $text-main;
font-size: 28rpx;
transition: all 0.25s ease;
}
.form-input:focus {
border-color: rgba(43, 142, 255, 0.42);
box-shadow: 0 0 0 8rpx rgba(43, 142, 255, 0.08);
}
.input-placeholder {
color: #B2BECC;
}
.picker-input {
display: flex;
align-items: center;
justify-content: space-between;
}
.picker-input-disabled {
background: rgba(43, 142, 255, 0.05);
}
.picker-fixed-text,
.text-gray {
color: $text-light;
}
.text-black {
color: $text-main;
}
.picker-arrow {
font-size: 28rpx;
color: #9AA7B6;
}
.role-switch-wrap {
margin-top: 18rpx;
}
.role-switch {
padding: 10rpx;
border-radius: 24rpx;
background: rgba(43, 142, 255, 0.06);
}
.role-tab {
padding: 22rpx 0 20rpx;
border-radius: 18rpx;
text-align: center;
color: $text-sub;
font-size: 28rpx;
transition: all 0.2s ease;
}
.role-tab-subtitle {
margin-top: 6rpx;
font-size: 20rpx;
color: $text-light;
}
.role-active {
background: #FFFFFF;
color: $brand-ink;
font-weight: 600;
box-shadow: 0 10rpx 24rpx rgba(82, 131, 197, 0.1);
}
.role-active .role-tab-subtitle {
color: rgba(15, 63, 120, 0.56);
}
.gender-btn {
height: 96rpx;
line-height: 96rpx;
border-radius: 24rpx;
text-align: center;
font-size: 28rpx;
background: #FFFFFF;
color: $text-sub;
border: 1px solid $line-soft;
}
.gender-active {
color: $brand-ink;
border-color: rgba(43, 142, 255, 0.3);
background: rgba(43, 142, 255, 0.08);
font-weight: 600;
}
.tag-item {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 120rpx;
height: 72rpx;
padding: 0 34rpx;
margin-right: 18rpx;
margin-bottom: 10rpx;
border-radius: 999rpx;
background: #FFFFFF;
border: 1px solid $line-soft;
color: $text-sub;
font-size: 26rpx;
transition: all 0.2s ease;
}
.tag-active {
background: rgba(43, 142, 255, 0.08);
border-color: rgba(43, 142, 255, 0.28);
color: $brand-ink;
font-weight: 600;
}
.upload-box {
min-height: 260rpx;
padding: 40rpx 32rpx;
border-radius: 28rpx;
border: 1px dashed rgba(43, 142, 255, 0.24);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(245, 249, 255, 0.88) 100%);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.upload-box:active {
transform: scale(0.995);
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.upload-icon-wrap {
width: 100rpx;
height: 100rpx;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(43, 142, 255, 0.1);
color: $brand;
font-size: 42rpx;
}
.upload-text {
margin-top: 22rpx;
font-size: 26rpx;
font-weight: 600;
color: $text-main;
}
.upload-subtext {
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.5;
color: $text-sub;
}
.attachment-file-card {
overflow: hidden;
border-radius: 28rpx;
background: #FFFFFF;
border: 1px solid rgba(43, 142, 255, 0.1);
box-shadow: 0 14rpx 34rpx rgba(88, 126, 180, 0.08);
}
.attachment-preview-image,
.attachment-file-cover {
width: 100%;
height: 320rpx;
display: block;
}
.attachment-file-cover {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, rgba(244, 248, 255, 1) 0%, rgba(236, 243, 252, 1) 100%);
}
.attachment-file-cover.is-unsupported {
color: $brand-ink;
}
.attachment-file-ext {
font-size: 54rpx;
font-weight: 700;
letter-spacing: 4rpx;
}
.attachment-file-info {
padding: 24rpx 24rpx 18rpx;
}
.attachment-name {
font-size: 28rpx;
font-weight: 600;
color: $text-main;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.attachment-type {
margin-top: 8rpx;
font-size: 24rpx;
line-height: 1.5;
color: $text-sub;
}
.attachment-action-row {
display: flex;
padding: 0 24rpx 24rpx;
}
.attachment-action-btn {
flex: 1;
height: 76rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(43, 142, 255, 0.04);
border: 1px solid rgba(43, 142, 255, 0.08);
color: $text-main;
font-size: 26rpx;
font-weight: 600;
}
.attachment-action-btn + .attachment-action-btn {
margin-left: 16rpx;
}
.attachment-action-btn.is-primary {
color: #FFFFFF;
background: linear-gradient(135deg, #2B8EFF 0%, #4CA0FF 100%);
border-color: transparent;
}
.bottom-btn-area {
position: fixed;
left: 20rpx;
right: 20rpx;
bottom: 18rpx;
bottom: calc(env(safe-area-inset-bottom) + 18rpx);
z-index: 20;
}
.action-dock {
padding: 20rpx;
border-radius: 34rpx;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.88);
box-shadow: 0 26rpx 70rpx rgba(74, 110, 170, 0.16);
backdrop-filter: blur(22rpx);
}
.custom-btn {
display: flex;
align-items: center;
justify-content: center;
border-radius: 999rpx;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.custom-btn:active {
transform: scale(0.992);
}
.submit-btn {
width: 100%;
height: 100rpx;
}
.btn-primary {
background: linear-gradient(135deg, #2B8EFF 0%, #4DA4FF 100%);
color: #FFFFFF;
box-shadow: 0 16rpx 30rpx rgba(43, 142, 255, 0.22);
}
.submit-btn-text {
font-size: 30rpx;
font-weight: 600;
letter-spacing: 1rpx;
}
.btn-disabled {
opacity: 0.56;
pointer-events: none;
}
.secondary-entry-card {
margin-top: 18rpx;
padding: 26rpx 24rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(245, 249, 255, 0.96);
border: 1px solid rgba(43, 142, 255, 0.1);
}
.secondary-entry-copy {
flex: 1;
min-width: 0;
}
.secondary-entry-title {
font-size: 28rpx;
font-weight: 600;
color: $text-main;
}
.secondary-entry-desc {
margin-top: 8rpx;
font-size: 23rpx;
line-height: 1.55;
color: $text-sub;
}
.secondary-entry-arrow {
margin-left: 20rpx;
font-size: 30rpx;
color: #8EA2B8;
}
.login-row {
display: flex;
align-items: center;
justify-content: center;
margin-top: 18rpx;
}
.login-text {
font-size: 24rpx;
color: $text-sub;
}
.login-link {
margin-left: 10rpx;
padding: 8rpx 16rpx;
border-radius: 999rpx;
background: rgba(43, 142, 255, 0.08);
color: $brand-ink;
font-size: 24rpx;
font-weight: 600;
}
.animation-fade-in {
opacity: 0;
animation: fadeIn 0.55s ease forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(18rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.privacy-mask {
position: fixed;
inset: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
background: rgba(18, 32, 51, 0.32);
backdrop-filter: blur(8rpx);
}
.privacy-dialog {
width: 100%;
max-width: 620rpx;
padding: 48rpx 40rpx 40rpx;
border-radius: 34rpx;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 26rpx 70rpx rgba(72, 108, 164, 0.18);
}
.privacy-title {
font-size: 34rpx;
font-weight: 600;
text-align: center;
color: $text-main;
}
.privacy-content {
margin-top: 28rpx;
font-size: 28rpx;
line-height: 1.72;
color: $text-sub;
}
.privacy-link {
color: $brand;
}
.privacy-action-row {
display: flex;
margin-top: 44rpx;
}
.privacy-action-btn {
flex: 1;
height: 90rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
color: $text-main;
background: rgba(244, 248, 255, 1);
border: 1px solid rgba(43, 142, 255, 0.12);
}
.privacy-action-btn + .privacy-action-btn {
margin-left: 18rpx;
}
button.privacy-action-btn {
padding: 0;
line-height: 90rpx;
}
button.privacy-action-btn::after {
border: none;
}
.privacy-action-btn.is-primary {
color: #FFFFFF;
background: linear-gradient(135deg, #2B8EFF 0%, #4DA4FF 100%);
border-color: transparent;
}
</style>