1740 lines
60 KiB
Vue
1740 lines
60 KiB
Vue
<template>
|
|
<view class="register-page tn-safe-area-inset-bottom">
|
|
<tn-nav-bar :isBack="true" :bottomShadow="false" :backgroundColor="navBackgroundColor">
|
|
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
|
|
</tn-nav-bar>
|
|
|
|
<view class="register-content" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
|
|
<view class="page-header animation-fade-in">
|
|
<view class="tn-text-bold tn-color-black page-title">{{ t('pageTitle') }}</view>
|
|
<view class="tn-color-gray tn-text-sm page-desc">
|
|
{{ t('pageDesc') }}
|
|
</view>
|
|
<view class="draft-tip-bar tn-margin-top-sm">
|
|
<text class="tn-icon-safe-fill draft-tip-icon"></text>
|
|
<text class="draft-tip-text">{{ draftTipText }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="form-card animation-fade-in" style="animation-delay: 0.1s;">
|
|
<view class="section-title tn-text-bold tn-text-lg">
|
|
<view class="title-indicator"></view>
|
|
{{ t('accountSection') }}
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('accountLabel') }} <text class="required-mark">*</text></view>
|
|
<input class="form-input" :placeholder="t('accountPlaceholder')" placeholder-class="input-placeholder" v-model="form.account" />
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('passwordLabel') }} <text class="required-mark">*</text></view>
|
|
<input class="form-input" type="password" :placeholder="t('passwordPlaceholder')" placeholder-class="input-placeholder" v-model="form.password" />
|
|
</view>
|
|
|
|
<view class="form-item" style="margin-bottom: 0;">
|
|
<view class="form-label">{{ t('emailLabel') }} <text class="required-mark">*</text></view>
|
|
<input class="form-input" :placeholder="t('emailPlaceholder')" placeholder-class="input-placeholder" v-model="form.email" />
|
|
</view>
|
|
|
|
<!-- 暂时屏蔽邮箱验证码 -->
|
|
<view class="form-item" style="margin-bottom: 0;" v-if="false">
|
|
<view class="form-label">{{ t('emailCodeLabel') }}</view>
|
|
<input class="form-input" :placeholder="t('emailCodePlaceholder')" placeholder-class="input-placeholder" v-model="form.emailVerifyCode" />
|
|
</view>
|
|
</view>
|
|
|
|
<view class="form-card animation-fade-in" style="animation-delay: 0.2s;">
|
|
<view class="section-title tn-text-bold tn-text-lg">
|
|
<view class="title-indicator"></view>
|
|
{{ t('basicSection') }}
|
|
</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')">
|
|
{{ t('foreignIdentity') }}
|
|
</view>
|
|
<view class="role-tab tn-flex-1" :class="form.identityType === 'DomesticStudents' ? 'role-active' : ''" @click="switchIdentityType('DomesticStudents')">
|
|
{{ t('domesticIdentity') }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('nameLabel') }} <text class="required-mark">*</text></view>
|
|
<input class="form-input" :placeholder="t('namePlaceholder')" placeholder-class="input-placeholder" v-model="form.name" />
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('genderLabel') }} <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">{{ t('phoneLabel') }} <text class="required-mark">*</text></view>
|
|
<input class="form-input" type="number" :placeholder="t('phonePlaceholder')" placeholder-class="input-placeholder" v-model="form.phone" />
|
|
</view>
|
|
|
|
<view class="form-item" style="margin-bottom: 0;">
|
|
<view class="form-label">{{ t('countryLabel') }} <text class="required-mark">*</text></view>
|
|
<view v-if="isDomesticIdentity" class="form-input picker-input picker-input-disabled">
|
|
<text class="text-black">{{ form.country || t('chinaText') }}</text>
|
|
<text class="picker-fixed-text">{{ t('countryFixedText') }}</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 || t('countryPlaceholder') }}</text>
|
|
<text class="tn-icon-right tn-color-gray"></text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="form-card animation-fade-in" style="animation-delay: 0.3s;">
|
|
<view class="section-title tn-text-bold tn-text-lg">
|
|
<view class="title-indicator"></view>
|
|
{{ t('profileSection') }}
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('workUnitLabel') }} <text class="required-mark">*</text></view>
|
|
<input class="form-input" :placeholder="t('workUnitPlaceholder')" placeholder-class="input-placeholder" v-model="form.workUnit" />
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('hangzhouPropertyLabel') }} <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">{{ t('yesText') }}</view>
|
|
<view class="tag-item" :class="form.hasHangzhouProperty === false ? 'tag-active' : ''" @click="form.hasHangzhouProperty = false">{{ t('noText') }}</view>
|
|
</view>
|
|
</view>
|
|
|
|
<template v-if="form.identityType === 'DomesticStudents'">
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('idCardLabel') }} <text class="required-mark">*</text></view>
|
|
<input class="form-input" :placeholder="t('idCardPlaceholder')" placeholder-class="input-placeholder" v-model="form.idCard" />
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('idCardFrontAttachmentLabel') }} <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="tn-margin-top-sm upload-text">{{ t('uploadIdFront') }}</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 tn-margin-top-xs">{{ t('imageUploadedTip') }}</view>
|
|
</view>
|
|
<view class="attachment-action-row">
|
|
<view class="attachment-action-btn" @click.stop="previewAttachment('idCardFrontAttachment')">
|
|
{{ t('viewImage') }}
|
|
</view>
|
|
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('idCardFrontAttachment')">
|
|
{{ t('reupload') }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="form-item" style="margin-bottom: 0;">
|
|
<view class="form-label">{{ t('idCardBackAttachmentLabel') }} <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="tn-margin-top-sm upload-text">{{ t('uploadIdBack') }}</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 tn-margin-top-xs">{{ t('imageUploadedTip') }}</view>
|
|
</view>
|
|
<view class="attachment-action-row">
|
|
<view class="attachment-action-btn" @click.stop="previewAttachment('idCardBackAttachment')">
|
|
{{ t('viewImage') }}
|
|
</view>
|
|
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('idCardBackAttachment')">
|
|
{{ t('reupload') }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('passportLabel') }} <text class="required-mark">*</text></view>
|
|
<input class="form-input" :placeholder="t('passportPlaceholder')" placeholder-class="input-placeholder" v-model="form.passportNumber" />
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('passportExpiryLabel') }} <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 || t('selectDate') }}</text>
|
|
<text class="tn-icon-calendar tn-color-gray"></text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('schoolLabel') }}</view>
|
|
<input class="form-input" :placeholder="t('schoolPlaceholder')" placeholder-class="input-placeholder" v-model="form.school" />
|
|
</view>
|
|
|
|
<view class="form-item" style="margin-bottom: 0;">
|
|
<view class="form-label">{{ t('passportAttachmentLabel') }} <text class="required-mark">*</text></view>
|
|
<view v-if="!form.passportAttachment.serverValue" class="upload-box" @click="uploadAttachment('passportAttachment')">
|
|
<view v-if="!form.passportAttachment.serverValue" class="upload-placeholder">
|
|
<view class="upload-icon-wrap"><text class="tn-icon-upload"></text></view>
|
|
<view class="tn-margin-top-sm upload-text">{{ t('uploadPassport') }}</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 tn-margin-top-xs">
|
|
{{ isImageAttachment(form.passportAttachment) ? t('imageUploadedTip') : t('passportImageOnlyTip') }}
|
|
</view>
|
|
</view>
|
|
<view class="attachment-action-row">
|
|
<view
|
|
v-if="isImageAttachment(form.passportAttachment)"
|
|
class="attachment-action-btn"
|
|
@click.stop="previewAttachment('passportAttachment')"
|
|
>
|
|
{{ t('viewImage') }}
|
|
</view>
|
|
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('passportAttachment')">
|
|
{{ t('reupload') }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
</view>
|
|
|
|
<view class="form-card animation-fade-in" style="animation-delay: 0.4s;">
|
|
<view class="section-title tn-text-bold tn-text-lg">
|
|
<view class="title-indicator"></view>
|
|
{{ t('preferenceSection') }}
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('dietaryLabel') }}</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 || t('dietaryPlaceholder') }}</text>
|
|
<text class="tn-icon-right tn-color-gray"></text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">{{ t('hobbyLabel') }}</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 || t('hobbyPlaceholder') }}</text>
|
|
<text class="tn-icon-right tn-color-gray"></text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
|
|
<view class="form-item" style="margin-bottom: 0;">
|
|
<view class="form-label">{{ t('religionLabel') }}</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 || t('religionPlaceholder') }}</text>
|
|
<text class="tn-icon-right tn-color-gray"></text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="bottom-btn-area tn-margin-top-xl animation-fade-in" style="animation-delay: 0.5s;">
|
|
<view class="custom-btn btn-primary submit-btn" @click="submitForm" :class="{ 'btn-disabled': isSubmitting }">
|
|
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? t('submitting') : t('submitRegister') }}</text>
|
|
</view>
|
|
|
|
<view class="secondary-entry-card tn-margin-top-lg" @click="goToRegistrationRecords">
|
|
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
|
|
<view>
|
|
<view class="secondary-entry-title">{{ t('viewRegistrationRecords') }}</view>
|
|
<view class="secondary-entry-desc tn-margin-top-xs">{{ t('viewRegistrationRecordsDesc') }}</view>
|
|
</view>
|
|
<text class="tn-icon-right secondary-entry-arrow"></text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="tn-flex tn-flex-row-center tn-margin-top-lg">
|
|
<view class="tn-color-gray tn-text-sm">{{ t('hasAccount') }}</view>
|
|
<view class="tn-color-black tn-text-bold tn-text-sm tn-margin-left-xs login-link" @click="goToLogin">{{ t('goLogin') }}</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</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 { 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: '',
|
|
idCard: '',
|
|
workUnit: '',
|
|
hasHangzhouProperty: null,
|
|
dietaryHabit: '',
|
|
hobby: '',
|
|
passportNumber: '',
|
|
passportExpiry: '',
|
|
school: '',
|
|
religion: '',
|
|
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,
|
|
hasHangzhouProperty: typeof source.hasHangzhouProperty === 'boolean' ? source.hasHangzhouProperty : null,
|
|
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: '',
|
|
form: createDefaultForm(),
|
|
rawCountryList: [],
|
|
rawHobbyList: [],
|
|
rawDietaryHabitList: [],
|
|
rawReligionList: [],
|
|
countryOptions: [],
|
|
hobbyOptions: [],
|
|
dietaryHabitOptions: [],
|
|
religionOptions: [],
|
|
i18n: {
|
|
zh: {
|
|
navTitle: '注册账号',
|
|
pageTitle: '注册并提交实名资料',
|
|
pageDesc: '账号信息和实名认证资料需要一起填写完成,提交的是注册申请单,审批通过后才算注册成功。',
|
|
accountSection: '账号信息',
|
|
accountLabel: '登录账号',
|
|
accountPlaceholder: '请输入登录账号',
|
|
passwordLabel: '登录密码',
|
|
passwordPlaceholder: '请输入 6 位及以上密码',
|
|
emailLabel: '邮箱',
|
|
emailPlaceholder: '请输入邮箱',
|
|
emailCodeLabel: '邮箱验证码',
|
|
emailCodePlaceholder: '请输入邮箱验证码',
|
|
basicSection: '基础实名信息',
|
|
foreignIdentity: '外籍人员',
|
|
domesticIdentity: '中国籍',
|
|
nameLabel: '姓名',
|
|
namePlaceholder: '请输入真实姓名',
|
|
genderLabel: '性别',
|
|
genderMale: '男',
|
|
genderFemale: '女',
|
|
phoneLabel: '手机号',
|
|
phonePlaceholder: '请输入手机号',
|
|
countryLabel: '国籍',
|
|
chinaText: '中国',
|
|
countryFixedText: '中国籍固定',
|
|
countryPlaceholder: '请选择国籍',
|
|
profileSection: '认证档案',
|
|
workUnitLabel: '工作单位',
|
|
workUnitPlaceholder: '请输入工作单位',
|
|
hangzhouPropertyLabel: '杭州有无房产',
|
|
yesText: '有',
|
|
noText: '无',
|
|
idCardLabel: '身份证号',
|
|
idCardPlaceholder: '请输入身份证号',
|
|
idCardFrontAttachmentLabel: '身份证正面附件',
|
|
idCardBackAttachmentLabel: '身份证反面附件',
|
|
uploadIdFront: '点击上传身份证正面',
|
|
uploadIdBack: '点击上传身份证反面',
|
|
imageUploadedTip: '已上传图片,可点击查看大图',
|
|
viewImage: '查看图片',
|
|
reupload: '重新上传',
|
|
passportLabel: '护照号',
|
|
passportPlaceholder: '请输入护照号',
|
|
passportExpiryLabel: '护照到期时间',
|
|
selectDate: '请选择日期',
|
|
schoolLabel: '就读学校',
|
|
schoolPlaceholder: '请输入就读学校(选填)',
|
|
passportAttachmentLabel: '护照附件',
|
|
uploadPassport: '点击上传护照图片',
|
|
passportImageOnlyTip: '护照附件仅支持图片,请重新上传',
|
|
preferenceSection: '偏好信息',
|
|
dietaryLabel: '饮食习惯',
|
|
hobbyLabel: '兴趣爱好',
|
|
religionLabel: '宗教信仰',
|
|
dietaryPlaceholder: '请选择饮食习惯',
|
|
hobbyPlaceholder: '请选择兴趣爱好',
|
|
religionPlaceholder: '请选择宗教信仰',
|
|
submitting: '提交中...',
|
|
submitRegister: '提交注册申请',
|
|
viewRegistrationRecords: '查看注册记录',
|
|
viewRegistrationRecordsDesc: '如果当前微信账号已提交过申请,可直接查看历史记录和审批状态',
|
|
hasAccount: '已有账号?',
|
|
goLogin: '去登录',
|
|
loading: '加载中...',
|
|
restoreDraftTitle: '发现未完成的数据',
|
|
restoreDraftContent: '是否填入上次未完成或提交失败时缓存的注册信息?',
|
|
restoreDraftConfirm: '填入',
|
|
restoreDraftCancel: '不用',
|
|
restoreDraftSuccess: '已填入上次失败数据',
|
|
countryLoadFailed: '国家列表加载失败',
|
|
dictLoadFailed: '字典加载失败,稍后可重试',
|
|
noSelectableOptions: '当前暂无可选项',
|
|
noSelectableCountry: '当前暂无可选国家',
|
|
uploading: '上传中...',
|
|
uploadSuccess: '上传成功',
|
|
uploadFailed: '上传失败,请重试',
|
|
attachmentNotImage: '当前附件不是图片',
|
|
uploadImageOption: '上传图片',
|
|
submitPending: '申请已提交,待审批',
|
|
submitFailed: '提交失败,请稍后重试',
|
|
draftTipDefault: '未完成内容会自动保存,可随时返回继续填写',
|
|
draftTipSaved: '当前填写内容已自动保存',
|
|
validateRequiredBasic: '请先填写账号、密码、邮箱和姓名',
|
|
validatePasswordLength: '登录密码至少 6 位',
|
|
validateEmail: '请输入正确的邮箱地址',
|
|
validateIdentityBasic: '请完善性别、手机号和国籍',
|
|
validatePhone: '请输入正确的手机号',
|
|
validateProfile: '请完善工作单位和杭州房产情况',
|
|
validateIdCard: '请填写身份证号',
|
|
validateIdCardAttachments: '请上传身份证正反面附件',
|
|
validatePassportBasic: '请填写护照号和护照到期时间',
|
|
validatePassportAttachment: '请上传护照图片',
|
|
validatePassportAttachmentImage: '护照附件仅支持图片格式'
|
|
},
|
|
en: {
|
|
navTitle: 'Register',
|
|
pageTitle: 'Register and Submit Verification',
|
|
pageDesc: 'Account information and identity verification materials must be completed together. Registration succeeds only after approval.',
|
|
accountSection: 'Account Information',
|
|
accountLabel: 'Account',
|
|
accountPlaceholder: 'Enter account name',
|
|
passwordLabel: 'Password',
|
|
passwordPlaceholder: 'Enter at least 6 characters',
|
|
emailLabel: 'Email',
|
|
emailPlaceholder: 'Enter email',
|
|
emailCodeLabel: 'Email Code',
|
|
emailCodePlaceholder: 'Enter email verification code',
|
|
basicSection: 'Basic Verification',
|
|
foreignIdentity: 'Foreign',
|
|
domesticIdentity: 'Chinese',
|
|
nameLabel: 'Name',
|
|
namePlaceholder: 'Enter your real name',
|
|
genderLabel: 'Gender',
|
|
genderMale: 'Male',
|
|
genderFemale: 'Female',
|
|
phoneLabel: 'Phone',
|
|
phonePlaceholder: 'Enter phone number',
|
|
countryLabel: 'Country',
|
|
chinaText: 'China',
|
|
countryFixedText: 'Fixed for Chinese users',
|
|
countryPlaceholder: 'Select country',
|
|
profileSection: 'Profile',
|
|
workUnitLabel: 'Work Unit',
|
|
workUnitPlaceholder: 'Enter work unit',
|
|
hangzhouPropertyLabel: 'Property in Hangzhou',
|
|
yesText: 'Yes',
|
|
noText: 'No',
|
|
idCardLabel: 'ID Card Number',
|
|
idCardPlaceholder: 'Enter ID card number',
|
|
idCardFrontAttachmentLabel: 'ID Card Front',
|
|
idCardBackAttachmentLabel: 'ID Card Back',
|
|
uploadIdFront: 'Upload ID card front',
|
|
uploadIdBack: 'Upload ID card back',
|
|
imageUploadedTip: 'Image uploaded, tap to preview',
|
|
viewImage: 'Preview',
|
|
reupload: 'Re-upload',
|
|
passportLabel: 'Passport Number',
|
|
passportPlaceholder: 'Enter passport number',
|
|
passportExpiryLabel: 'Passport Expiry Date',
|
|
selectDate: 'Select date',
|
|
schoolLabel: 'School',
|
|
schoolPlaceholder: 'Enter school (optional)',
|
|
passportAttachmentLabel: 'Passport Attachment',
|
|
uploadPassport: 'Upload passport image',
|
|
passportImageOnlyTip: 'Passport attachment only supports images, please re-upload',
|
|
preferenceSection: 'Preferences',
|
|
dietaryLabel: 'Dietary Habit',
|
|
hobbyLabel: 'Hobby',
|
|
religionLabel: 'Religion',
|
|
dietaryPlaceholder: 'Select dietary habit',
|
|
hobbyPlaceholder: 'Select hobby',
|
|
religionPlaceholder: 'Select religion',
|
|
submitting: 'Submitting...',
|
|
submitRegister: 'Submit Registration',
|
|
viewRegistrationRecords: 'View Registration Records',
|
|
viewRegistrationRecordsDesc: 'If this Wechat account has submitted before, you can review the history and approval status directly.',
|
|
hasAccount: 'Already have an account?',
|
|
goLogin: 'Sign In',
|
|
loading: 'Loading...',
|
|
restoreDraftTitle: 'Unfinished Data Found',
|
|
restoreDraftContent: 'Would you like to restore the last unfinished or failed registration data?',
|
|
restoreDraftConfirm: 'Restore',
|
|
restoreDraftCancel: 'No',
|
|
restoreDraftSuccess: 'Previous draft restored',
|
|
countryLoadFailed: 'Failed to load country list',
|
|
dictLoadFailed: 'Failed to load dictionaries',
|
|
noSelectableOptions: 'No selectable options',
|
|
noSelectableCountry: 'No selectable country',
|
|
uploading: 'Uploading...',
|
|
uploadSuccess: 'Upload successful',
|
|
uploadFailed: 'Upload failed, please retry',
|
|
attachmentNotImage: 'The current attachment is not an image',
|
|
uploadImageOption: 'Upload Image',
|
|
submitPending: 'Application submitted, pending approval',
|
|
submitFailed: 'Submission failed, please try again later',
|
|
draftTipDefault: 'Unfinished content is auto-saved and can be continued later',
|
|
draftTipSaved: 'Current content has been auto-saved',
|
|
validateRequiredBasic: 'Please complete the account, password, email, and name fields first',
|
|
validatePasswordLength: 'Password must be at least 6 characters',
|
|
validateEmail: 'Please enter a valid email address',
|
|
validateIdentityBasic: 'Please complete gender, phone number, and country',
|
|
validatePhone: 'Please enter a valid phone number',
|
|
validateProfile: 'Please complete work unit and property status in Hangzhou',
|
|
validateIdCard: 'Please enter your ID card number',
|
|
validateIdCardAttachments: 'Please upload both sides of the ID card',
|
|
validatePassportBasic: 'Please enter passport number and expiry date',
|
|
validatePassportAttachment: 'Please upload the passport image',
|
|
validatePassportAttachmentImage: 'Passport attachment only supports image files'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
language() {
|
|
return this.$getLanguage()
|
|
},
|
|
navBackgroundColor() {
|
|
return `rgba(255, 255, 255, ${this.navBgOpacity})`
|
|
},
|
|
hobbyRange() {
|
|
return this.hobbyOptions.length ? this.hobbyOptions.map(item => item.value) : [this.t('noSelectableOptions')]
|
|
},
|
|
countryRange() {
|
|
return this.countryOptions.length ? this.countryOptions.map(item => item.value) : [this.t('noSelectableOptions')]
|
|
},
|
|
dietaryHabitRange() {
|
|
return this.dietaryHabitOptions.length ? this.dietaryHabitOptions.map(item => item.value) : [this.t('noSelectableOptions')]
|
|
},
|
|
isDomesticIdentity() {
|
|
return this.form.identityType === 'DomesticStudents'
|
|
},
|
|
religionRange() {
|
|
return this.religionOptions.length ? this.religionOptions.map(item => item.value) : [this.t('noSelectableOptions')]
|
|
},
|
|
genderOptions() {
|
|
return [
|
|
{ key: 1, value: this.t('genderMale') },
|
|
{ key: 0, value: this.t('genderFemale') }
|
|
]
|
|
}
|
|
},
|
|
watch: {
|
|
language() {
|
|
this.rebuildLocalizedOptions()
|
|
this.draftTipText = this.t('draftTipDefault')
|
|
},
|
|
form: {
|
|
deep: true,
|
|
handler() {
|
|
this.scheduleDraftPersist()
|
|
}
|
|
}
|
|
},
|
|
async onLoad(options) {
|
|
this.draftTipText = this.t('draftTipDefault')
|
|
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()
|
|
},
|
|
onPageScroll(e) {
|
|
let opacity = e.scrollTop / 80
|
|
if (opacity > 1) {
|
|
opacity = 1
|
|
} else if (opacity < 0) {
|
|
opacity = 0
|
|
}
|
|
this.navBgOpacity = opacity
|
|
},
|
|
methods: {
|
|
t(key) {
|
|
return this.$tm('pkgAccount.register.register', key)
|
|
},
|
|
isChinaCountry(value = '') {
|
|
const text = String(value || '').trim()
|
|
return text === '中国' || text === 'China' || text === this.t('chinaText')
|
|
},
|
|
async checkPendingRegistration(bypassRedirect = false) {
|
|
uni.showLoading({ title: this.t('loading') })
|
|
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.t('restoreDraftTitle'),
|
|
content: this.t('restoreDraftContent'),
|
|
confirmText: this.t('restoreDraftConfirm'),
|
|
cancelText: this.t('restoreDraftCancel'),
|
|
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
|
|
}
|
|
this.applyIdentityCountryRule()
|
|
uni.showToast({
|
|
title: this.t('restoreDraftSuccess'),
|
|
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 = [
|
|
'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.t('draftTipDefault')
|
|
return
|
|
}
|
|
saveFailedRegistrationDraft({
|
|
form: this.buildFailedDraft(),
|
|
updatedAt: Date.now()
|
|
})
|
|
if (!isSilent) {
|
|
this.draftTipText = this.t('draftTipSaved')
|
|
}
|
|
},
|
|
switchIdentityType(type) {
|
|
if (this.form.identityType === type) return
|
|
if (type === 'DomesticStudents' && this.form.country) {
|
|
this.lastForeignCountry = this.form.country
|
|
}
|
|
this.form.identityType = type
|
|
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.t('countryLoadFailed'),
|
|
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())
|
|
])
|
|
.then(([hobbyRes, dietaryRes, religionRes]) => {
|
|
this.rawHobbyList = this.extractResponseList(hobbyRes)
|
|
this.rawDietaryHabitList = this.extractResponseList(dietaryRes)
|
|
this.rawReligionList = this.extractResponseList(religionRes)
|
|
this.hobbyOptions = this.normalizeDictList(this.rawHobbyList)
|
|
this.dietaryHabitOptions = this.normalizeDictList(this.rawDietaryHabitList)
|
|
this.religionOptions = this.normalizeDictList(this.rawReligionList)
|
|
})
|
|
.catch((error) => {
|
|
console.error('load dict options failed', error)
|
|
uni.showToast({
|
|
title: this.t('dictLoadFailed'),
|
|
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.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.applyIdentityCountryRule()
|
|
},
|
|
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.t('chinaText')
|
|
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.t('noSelectableOptions'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
const selected = options[event.detail.value]
|
|
this.form[field] = selected ? selected.value : ''
|
|
},
|
|
onCountryChange(event) {
|
|
if (this.isDomesticIdentity) return
|
|
if (!this.countryOptions.length) {
|
|
uni.showToast({
|
|
title: this.t('noSelectableCountry'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
const selected = this.countryOptions[event.detail.value]
|
|
this.form.country = selected ? selected.value : ''
|
|
this.lastForeignCountry = this.form.country || ''
|
|
},
|
|
uploadAttachment(field) {
|
|
return this.chooseImageFile()
|
|
.then((file) => {
|
|
if (!file || !file.path) return
|
|
uni.showLoading({
|
|
title: this.t('uploading')
|
|
})
|
|
|
|
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.t('uploadSuccess'),
|
|
icon: 'success'
|
|
})
|
|
})
|
|
})
|
|
.catch((error) => {
|
|
if (error && error.cancel) return
|
|
console.error('upload attachment failed', error)
|
|
uni.showToast({
|
|
title: this.t('uploadFailed'),
|
|
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.t('attachmentNotImage'),
|
|
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.t('submitting')
|
|
})
|
|
|
|
const authProfile = this.buildAuthProfile()
|
|
|
|
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.t('submitPending'),
|
|
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.t('submitFailed'),
|
|
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'
|
|
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.t('validateRequiredBasic')
|
|
}
|
|
if (this.form.password.length < 6) {
|
|
return this.t('validatePasswordLength')
|
|
}
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.form.email)) {
|
|
return this.t('validateEmail')
|
|
}
|
|
if (this.form.gender === null || !this.form.phone || !this.form.country) {
|
|
return this.t('validateIdentityBasic')
|
|
}
|
|
if (!/^1\d{10}$/.test(this.form.phone)) {
|
|
return this.t('validatePhone')
|
|
}
|
|
if (!this.form.workUnit || this.form.hasHangzhouProperty === null) {
|
|
return this.t('validateProfile')
|
|
}
|
|
|
|
if (this.form.identityType === 'DomesticStudents') {
|
|
if (!this.form.idCard) {
|
|
return this.t('validateIdCard')
|
|
}
|
|
if (!this.form.idCardFrontAttachment.serverValue || !this.form.idCardBackAttachment.serverValue) {
|
|
return this.t('validateIdCardAttachments')
|
|
}
|
|
return ''
|
|
}
|
|
|
|
if (!this.form.passportNumber || !this.form.passportExpiry) {
|
|
return this.t('validatePassportBasic')
|
|
}
|
|
if (!this.form.passportAttachment.serverValue) {
|
|
return this.t('validatePassportAttachment')
|
|
}
|
|
if (!this.isImageAttachment(this.form.passportAttachment)) {
|
|
return this.t('validatePassportAttachmentImage')
|
|
}
|
|
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.t('genderMale') : this.form.gender === 0 ? this.t('genderFemale') : ''),
|
|
phone: this.form.phone,
|
|
country: this.form.country,
|
|
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),
|
|
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,
|
|
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,
|
|
passportAttachment: profile.passportAttachment || null,
|
|
idCardFrontAttachment: profile.idCardFrontAttachment || null,
|
|
idCardBackAttachment: profile.idCardBackAttachment || null
|
|
}
|
|
},
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.register-page {
|
|
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.register-content {
|
|
padding: 0 24rpx 60rpx;
|
|
}
|
|
|
|
.page-header {
|
|
margin-top: 40rpx;
|
|
margin-bottom: 40rpx;
|
|
padding: 0 10rpx;
|
|
}
|
|
.page-title {
|
|
font-size: 56rpx;
|
|
letter-spacing: 2rpx;
|
|
color: #1A2233;
|
|
line-height: 1.25;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.page-desc {
|
|
margin-top: 16rpx;
|
|
line-height: 1.6;
|
|
color: #6A7A99;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.draft-tip-bar {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10rpx;
|
|
padding: 12rpx 18rpx;
|
|
border-radius: 999rpx;
|
|
background: rgba($brand-primary, 0.08);
|
|
border: 1rpx solid rgba($brand-primary, 0.12);
|
|
}
|
|
|
|
.draft-tip-icon {
|
|
color: $brand-primary;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.draft-tip-text {
|
|
color: #5C6B8A;
|
|
font-size: 24rpx;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.tip-text {
|
|
margin-top: 12rpx;
|
|
color: #5C6B8A;
|
|
line-height: 1.6;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
/* 卡片式表单 */
|
|
.form-card {
|
|
background-color: #FFFFFF;
|
|
border-radius: 24rpx;
|
|
padding: 40rpx 32rpx;
|
|
margin-bottom: 30rpx;
|
|
border: 1rpx solid rgba($brand-primary, 0.05);
|
|
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.section-title {
|
|
margin-bottom: 36rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
color: #1A2233;
|
|
font-size: 32rpx;
|
|
}
|
|
|
|
.title-indicator {
|
|
width: 8rpx;
|
|
height: 32rpx;
|
|
background-color: $brand-primary;
|
|
border-radius: 4rpx;
|
|
margin-right: 16rpx;
|
|
}
|
|
|
|
.form-item {
|
|
margin-bottom: 36rpx;
|
|
}
|
|
|
|
/* 表单元素 */
|
|
.form-label {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #1A2233;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.required-mark {
|
|
color: #FF4D4F;
|
|
margin-left: 6rpx;
|
|
font-weight: normal;
|
|
}
|
|
|
|
.form-input {
|
|
background-color: #FFFFFF;
|
|
height: 96rpx;
|
|
border-radius: 20rpx;
|
|
padding: 0 32rpx;
|
|
font-size: 28rpx;
|
|
border: 1rpx solid rgba($brand-primary, 0.15);
|
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
|
transition: all 0.3s ease;
|
|
box-sizing: border-box;
|
|
color: #1A2233;
|
|
}
|
|
|
|
.form-input:focus {
|
|
border: 1rpx solid $brand-primary;
|
|
background-color: #FFFFFF;
|
|
box-shadow: 0 0 0 6rpx rgba($brand-primary, 0.1);
|
|
}
|
|
|
|
.input-placeholder {
|
|
color: #A0B0C0;
|
|
}
|
|
|
|
.picker-input {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.picker-input-disabled {
|
|
background-color: #F7F9FC;
|
|
color: #6A7A99;
|
|
}
|
|
|
|
.picker-fixed-text {
|
|
color: #8A99B3;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.text-black {
|
|
color: #1A2233;
|
|
}
|
|
|
|
.text-gray {
|
|
color: #A0B0C0;
|
|
}
|
|
|
|
/* 双标签切换 */
|
|
.role-switch-wrap {
|
|
margin-bottom: 36rpx;
|
|
}
|
|
.role-switch {
|
|
background-color: rgba($brand-primary, 0.05);
|
|
border-radius: 24rpx;
|
|
padding: 10rpx;
|
|
border: 1rpx solid rgba($brand-primary, 0.1);
|
|
}
|
|
.role-tab {
|
|
border-radius: 18rpx;
|
|
color: #6A7A99;
|
|
padding: 18rpx 0;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
text-align: center;
|
|
font-weight: bold;
|
|
font-size: 28rpx;
|
|
}
|
|
.role-active {
|
|
background-color: #FFFFFF;
|
|
color: $brand-primary;
|
|
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.15);
|
|
}
|
|
|
|
/* 性别按钮 */
|
|
.gender-btn {
|
|
background-color: #FFFFFF;
|
|
height: 96rpx;
|
|
line-height: 96rpx;
|
|
border-radius: 20rpx;
|
|
font-size: 28rpx;
|
|
color: #6A7A99;
|
|
border: 1rpx solid rgba($brand-primary, 0.15);
|
|
transition: all 0.2s;
|
|
text-align: center;
|
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
|
}
|
|
.gender-active {
|
|
background-color: #FFFFFF;
|
|
border-color: $brand-primary;
|
|
color: $brand-primary;
|
|
font-weight: bold;
|
|
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
|
}
|
|
|
|
/* 标签 */
|
|
.tag-item {
|
|
display: inline-block;
|
|
background-color: #FFFFFF;
|
|
padding: 16rpx 36rpx;
|
|
border-radius: 40rpx;
|
|
font-size: 26rpx;
|
|
color: #6A7A99;
|
|
margin-right: 20rpx;
|
|
margin-bottom: 20rpx;
|
|
transition: all 0.2s;
|
|
border: 1rpx solid rgba($brand-primary, 0.15);
|
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
|
}
|
|
.tag-active {
|
|
background-color: #FFFFFF;
|
|
border: 1rpx solid $brand-primary;
|
|
color: $brand-primary;
|
|
font-weight: bold;
|
|
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
|
}
|
|
|
|
/* 附件上传框 */
|
|
.upload-box {
|
|
background-color: #FFFFFF;
|
|
border: 1rpx dashed rgba($brand-primary, 0.4);
|
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
|
border-radius: 24rpx;
|
|
min-height: 240rpx;
|
|
padding: 32rpx;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.upload-box:active {
|
|
background-color: rgba($brand-primary, 0.02);
|
|
border: 1rpx dashed rgba($brand-primary, 0.6);
|
|
}
|
|
|
|
.upload-placeholder {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.upload-icon-wrap {
|
|
width: 88rpx;
|
|
height: 88rpx;
|
|
background-color: #FFFFFF;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
|
color: $brand-primary;
|
|
font-size: 40rpx;
|
|
}
|
|
|
|
.upload-text {
|
|
color: #6A7A99;
|
|
font-size: 26rpx;
|
|
}
|
|
|
|
.attachment-preview {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.success-icon {
|
|
font-size: 64rpx;
|
|
color: $brand-primary;
|
|
margin-bottom: 16rpx;
|
|
}
|
|
|
|
.attachment-name {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #1A2233;
|
|
max-width: 400rpx;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.attachment-type {
|
|
color: #8A99B3;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.attachment-file-card {
|
|
background-color: #FFFFFF;
|
|
border: 1rpx solid rgba($brand-primary, 0.12);
|
|
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.06);
|
|
border-radius: 24rpx;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.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(135deg, rgba($brand-primary, 0.14) 0%, rgba($brand-primary, 0.06) 100%);
|
|
}
|
|
|
|
.attachment-file-cover.is-unsupported {
|
|
color: $brand-primary;
|
|
}
|
|
|
|
.attachment-file-ext {
|
|
font-size: 56rpx;
|
|
font-weight: bold;
|
|
letter-spacing: 4rpx;
|
|
}
|
|
|
|
.attachment-file-info {
|
|
padding: 24rpx 24rpx 12rpx;
|
|
}
|
|
|
|
.attachment-action-row {
|
|
display: flex;
|
|
padding: 0 24rpx 24rpx;
|
|
}
|
|
|
|
.attachment-action-btn {
|
|
flex: 1;
|
|
height: 76rpx;
|
|
border-radius: 18rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
color: $brand-primary;
|
|
background: rgba($brand-primary, 0.08);
|
|
border: 1rpx solid rgba($brand-primary, 0.12);
|
|
}
|
|
|
|
.attachment-action-btn + .attachment-action-btn {
|
|
margin-left: 16rpx;
|
|
}
|
|
|
|
.attachment-action-btn.is-primary {
|
|
color: #FFFFFF;
|
|
background: linear-gradient(135deg, rgba($brand-primary, 0.92) 0%, $brand-primary 100%);
|
|
border-color: transparent;
|
|
}
|
|
|
|
/* 动画 */
|
|
.animation-fade-in {
|
|
opacity: 0;
|
|
animation: fadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(30rpx); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* 自定义按钮样式 */
|
|
.custom-btn {
|
|
display: inline-flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-radius: 100rpx;
|
|
padding: 12rpx 36rpx;
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
.submit-btn {
|
|
width: 100%;
|
|
height: 100rpx;
|
|
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
|
|
}
|
|
.custom-btn:active {
|
|
transform: scale(0.96);
|
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.2);
|
|
}
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
|
|
color: #ffffff;
|
|
}
|
|
|
|
.btn-disabled {
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.secondary-entry-card {
|
|
background: #FFFFFF;
|
|
border-radius: 24rpx;
|
|
padding: 28rpx 32rpx;
|
|
border: 1rpx solid rgba($brand-primary, 0.12);
|
|
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.05);
|
|
}
|
|
|
|
.secondary-entry-card:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.secondary-entry-title {
|
|
font-size: 30rpx;
|
|
font-weight: 700;
|
|
color: #1A2233;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.secondary-entry-desc {
|
|
font-size: 24rpx;
|
|
color: #6A7A99;
|
|
line-height: 1.5;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.secondary-entry-arrow {
|
|
color: $brand-primary;
|
|
font-size: 28rpx;
|
|
}
|
|
|
|
.login-link {
|
|
padding: 10rpx;
|
|
}
|
|
.login-link:active {
|
|
opacity: 0.7;
|
|
}
|
|
</style>
|