Files
QSY-lvjuxcx/pkgAccount/register/register.vue
liangjian 9fee2c3a74 feat: 优化首页服务入口与租住业务流程
补齐首页待办跳转、押金支付引导和多类租住服务页面能力,统一租客端关键业务流程体验。
2026-08-25 13:53:12 +08:00

2522 lines
80 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="register-intro animation-fade-in">
<view class="intro-kicker-row">
<view class="hero-kicker">{{ $tr('绿居公寓登记入口') }}</view>
</view>
<view class="page-title">{{ $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>
<view class="progress-strip animation-fade-in" style="animation-delay: 0.08s;">
<view
v-for="step in stepList"
:key="step.index"
class="progress-node"
:class="{
'is-active': currentStep === step.index,
'is-completed': currentStep > step.index
}"
>
<text class="progress-node-index">{{ step.code }}</text>
<text class="progress-node-text">{{ step.shortTitle }}</text>
</view>
</view>
<view v-if="currentStep === 1" 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 v-if="currentStep === 2" class="form-card animation-fade-in" style="animation-delay: 0.12s;">
<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('手机号') }}</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>
<view v-else class="form-input picker-input" @click="openCountrySelector">
<view class="picker-text-wrap">
<text :class="form.country ? 'text-black' : 'text-gray'">{{ form.country || $tr('请选择国籍') }}</text>
<text v-if="form.country" class="picker-subtext">{{ $tr('支持模糊搜索') }}</text>
</view>
<view class="picker-action-wrap">
<text v-if="form.country" class="picker-action-text">{{ $tr('重新选择') }}</text>
<text class="tn-icon-right picker-arrow"></text>
</view>
</view>
</view>
</view>
</view>
<view v-if="currentStep === 3" class="form-card animation-fade-in" style="animation-delay: 0.12s;">
<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('生日') }}</view>
<picker mode="date" :value="form.birthday" @change="onBirthdayChange">
<view class="form-input picker-input">
<text :class="form.birthday ? 'text-black' : 'text-gray'">{{ form.birthday || $tr('请选择生日') }}</text>
<text class="tn-icon-calendar picker-arrow"></text>
</view>
</picker>
</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">{{ $tr('生日') }}</view>
<picker mode="date" :value="form.birthday" @change="onBirthdayChange">
<view class="form-input picker-input">
<text :class="form.birthday ? 'text-black' : 'text-gray'">{{ form.birthday || $tr('请选择生日') }}</text>
<text class="tn-icon-calendar picker-arrow"></text>
</view>
</picker>
</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 v-if="currentStep === 4" class="form-card animation-fade-in" style="animation-delay: 0.12s;">
<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="step-action-row" :class="{ 'single-action': currentStep === 1 }">
<view v-if="currentStep > 1" class="custom-btn btn-secondary step-btn" @click="goPrevStep">
<text class="step-btn-text">{{ $tr('上一步') }}</text>
</view>
<view class="custom-btn btn-primary submit-btn step-btn" @click="handlePrimaryAction" :class="{ 'btn-disabled': isSubmitting }">
<text class="submit-btn-text">{{ primaryActionText }}</text>
</view>
</view>
</view>
</view>
<tn-popup v-model="showCountrySelector" mode="bottom" length="78%" :borderRadius="32" :closeBtn="true" @close="closeCountrySelector">
<view class="country-selector-popup">
<view class="country-selector-header">
<view class="country-selector-title">{{ $tr('选择国籍') }}</view>
<view class="country-selector-subtitle">{{ $tr('可按中文、英文或国家代码搜索') }}</view>
</view>
<view class="country-search-box">
<text class="tn-icon-search country-search-icon"></text>
<input
v-model.trim="countrySearchKeyword"
class="country-search-input"
:placeholder="$tr('搜索国家、地区或代码')"
placeholder-class="input-placeholder"
confirm-type="search"
/>
<view v-if="countrySearchKeyword" class="country-search-clear" @click="clearCountrySearch">{{ $tr('清空') }}</view>
</view>
<scroll-view scroll-y class="country-selector-list">
<view
v-for="(item, index) in filteredCountryOptions"
:key="index"
class="country-option"
:class="{ 'is-active': form.country === item.value }"
@click="selectCountryOptionByIndex(index)"
>
<view class="country-option-main">
<view class="country-option-name">{{ item.value }}</view>
<view v-if="item.secondaryText" class="country-option-meta">{{ item.secondaryText }}</view>
</view>
<text v-if="form.country === item.value" class="tn-icon-success-circle-fill country-option-check"></text>
</view>
<view v-if="!filteredCountryOptions.length" class="country-selector-empty">
{{ $tr('没有找到匹配的国家或地区') }}
</view>
</scroll-view>
</view>
</tn-popup>
<!-- #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'
import test from '../../tuniao-ui/libs/function/test.js'
function createAttachmentState() {
return {
name: '',
serverValue: '',
localPath: ''
}
}
function createDefaultForm() {
return {
identityType: 'ForeignStudents',
account: '',
password: '',
email: '',
emailVerifyCode: '',
name: '',
gender: 1,
phone: '',
country: '',
idCardType: 2,
idCard: '',
birthday: '',
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)
}
}
function normalizeRegistrationDraftStep(draft) {
const step = Number(draft && draft.currentStep)
if (step >= 1 && step <= 4) {
return step
}
return 1
}
export default {
data() {
return {
navBgOpacity: 0,
currentStep: 1,
isSubmitting: false,
dictLoading: false,
hasPromptedFailedDraft: false,
draftPersistTimer: null,
draftTipText: '',
lastForeignCountry: '',
lastForeignIdCardType: 2,
showCountrySelector: false,
countrySearchKeyword: '',
showPrivacyPopup: false,
resolvePrivacyAuthorization: null,
form: createDefaultForm(),
rawCountryList: [],
rawHobbyList: [],
rawDietaryHabitList: [],
rawReligionList: [],
rawIdCardTypeList: [],
countryOptions: [],
hobbyOptions: [],
dietaryHabitOptions: [],
religionOptions: [],
idCardTypeOptions: []
}
},
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.$tr('当前暂无可选项')]
},
filteredCountryOptions() {
if (!this.countryOptions.length) {
return []
}
const keyword = this.normalizeSearchText(this.countrySearchKeyword)
if (!keyword) {
return this.countryOptions
}
const compactKeyword = keyword.replace(/\s+/g, '')
return this.countryOptions.filter((item) => {
const searchText = this.normalizeSearchText(item.searchText || '')
const compactSearchText = searchText.replace(/\s+/g, '')
return searchText.includes(keyword) || compactSearchText.includes(compactKeyword)
})
},
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('证件附件仅支持图片,请重新上传')
},
stepList() {
return [
{
index: 1,
code: '01',
shortTitle: this.$tr('账号'),
title: this.$tr('账号信息'),
desc: this.$tr('先完成登录账号、密码和邮箱信息。')
},
{
index: 2,
code: '02',
shortTitle: this.$tr('实名'),
title: this.$tr('基础实名信息'),
desc: this.$tr('填写姓名、身份、手机号和国籍等基础资料。')
},
{
index: 3,
code: '03',
shortTitle: this.$tr('档案'),
title: this.$tr('认证档案'),
desc: this.$tr('补充证件、附件和审核所需资料。')
},
{
index: 4,
code: '04',
shortTitle: this.$tr('偏好'),
title: this.$tr('偏好信息'),
desc: this.$tr('最后补充选填偏好信息,然后完成提交。')
}
]
},
totalSteps() {
return this.stepList.length
},
currentStepMeta() {
return this.stepList.find(step => step.index === this.currentStep) || this.stepList[0]
},
isLastStep() {
return this.currentStep === this.totalSteps
},
primaryActionText() {
if (this.isLastStep) {
return this.isSubmitting ? this.$tr('提交中...') : this.$tr('完成并提交')
}
return this.$tr('下一步')
},
genderOptions() {
return [
{ key: 1, value: this.$tr('男') },
{ key: 0, value: this.$tr('女') }
]
}
},
watch: {
language() {
this.rebuildLocalizedOptions()
this.draftTipText = this.$tr('未完成内容会自动保存,可随时返回继续填写')
},
'form.idCard'(value) {
this.syncBirthdayFromIdCard(value)
},
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)
this.currentStep = normalizeRegistrationDraftStep(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',
'birthday',
'country',
'idCard',
'workUnit',
'dietaryHabit',
'hobby',
'passportNumber',
'passportExpiry',
'school',
'religion'
]
if (this.form.identityType !== 'ForeignStudents') {
return true
}
if (this.form.gender !== 1 || 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(),
currentStep: this.currentStep,
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
if (type === 'DomesticStudents') {
this.closeCountrySelector()
}
this.applyIdentityDocumentTypeRule()
this.applyIdentityCountryRule()
this.syncBirthdayFromIdCard(this.form.idCard)
},
onBirthdayChange(e) {
this.form.birthday = e.detail.value
},
normalizeIdCardValue(value = '') {
return String(value || '').trim().toUpperCase()
},
isValidBirthdayValue(value = '') {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return false
}
const [year, month, day] = value.split('-').map(item => Number(item))
const date = new Date(year, month - 1, day)
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
},
extractBirthdayFromIdCard(idCard = '') {
const normalizedIdCard = this.normalizeIdCardValue(idCard)
if (!test.idCard(normalizedIdCard)) {
return ''
}
const birthday = `${normalizedIdCard.slice(6, 10)}-${normalizedIdCard.slice(10, 12)}-${normalizedIdCard.slice(12, 14)}`
return this.isValidBirthdayValue(birthday) ? birthday : ''
},
syncBirthdayFromIdCard(idCard = '') {
if (!this.isDomesticIdentity) {
return
}
const birthday = this.extractBirthdayFromIdCard(idCard)
if (birthday) {
this.form.birthday = birthday
}
},
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 || ''
},
normalizeSearchText(value = '') {
return String(value || '').trim().toLowerCase().replace(/\s+/g, ' ')
},
getCountrySecondaryText(item = {}, localizedValue = '') {
const isEn = this.language === 'en'
const alternateCandidates = isEn
? [item.cname, item.nameZh, item.labelZh, item.valueZh, item.description]
: [item.ename, item.nameEn, item.labelEn, item.valueEn, item.descriptionEn]
const alternateText = alternateCandidates
.map(value => String(value || '').trim())
.find(value => value && value !== localizedValue)
const code = String(item.code || item.key || '').trim().toUpperCase()
return [alternateText, code].filter(Boolean).join(' · ')
},
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) => {
const value = this.getLocalizedOptionText(item)
const searchCandidates = this.getOptionTextCandidates(item)
return {
key: item.code || item.key || item.cname || '',
value,
secondaryText: this.getCountrySecondaryText(item, value),
searchText: searchCandidates.join(' ')
}
})
.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 || ''
}
},
openCountrySelector() {
if (this.isDomesticIdentity) {
return
}
if (!this.countryOptions.length) {
uni.showToast({
title: this.$tr('当前暂无可选国家'),
icon: 'none'
})
return
}
this.countrySearchKeyword = ''
this.showCountrySelector = true
},
closeCountrySelector() {
this.showCountrySelector = false
this.countrySearchKeyword = ''
},
clearCountrySearch() {
this.countrySearchKeyword = ''
},
selectCountryOptionByIndex(index) {
const option = (this.filteredCountryOptions || [])[Number(index)]
this.selectCountryOption(option)
},
selectCountryOption(option) {
if (!option || !option.value) {
return
}
this.form.country = option.value
this.lastForeignCountry = this.form.country || ''
this.closeCountrySelector()
},
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
}
},
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() : ''
},
scrollToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: 220
})
},
validateStep(step = this.currentStep) {
if (step === 1) {
if (!this.form.account || !this.form.password || !this.form.email) {
return this.$tr('请先填写账号、密码和邮箱')
}
if (this.form.password.length < 6) {
return this.$tr('登录密码至少 6 位')
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.form.email)) {
return this.$tr('请输入正确的邮箱地址')
}
return ''
}
if (step === 2) {
if (!this.form.name) {
return this.$tr('请填写真实姓名')
}
if (this.form.gender === null || !this.form.country) {
return this.$tr('请完善性别和国籍')
}
return ''
}
if (step === 3) {
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 ''
},
goPrevStep() {
if (this.currentStep <= 1) {
return
}
this.currentStep -= 1
this.scrollToTop()
},
goNextStep() {
const errorMessage = this.validateStep(this.currentStep)
if (errorMessage) {
uni.showToast({
title: errorMessage,
icon: 'none'
})
return
}
if (this.currentStep >= this.totalSteps) {
return
}
this.currentStep += 1
this.scrollToTop()
},
handlePrimaryAction() {
if (this.isLastStep) {
this.submitForm()
return
}
this.goNextStep()
},
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) => {
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() {
for (let step = 1; step <= 3; step += 1) {
const errorMessage = this.validateStep(step)
if (errorMessage) {
this.currentStep = step
this.scrollToTop()
return errorMessage
}
}
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.normalizeIdCardValue(this.form.idCard) : '',
birthday: this.form.identityType === 'DomesticStudents'
? this.extractBirthdayFromIdCard(this.form.idCard) || this.form.birthday || ''
: this.form.birthday || '',
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 || null,
country: profile.country,
idCardType: profile.idCardType || null,
IdCardType: profile.idCardType || null,
idCard: profile.idCard || null,
birthday: profile.birthday || 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: $brand-primary;
$brand-soft: rgba($brand-primary, 0.12);
$brand-ink: $brand-primary;
$bg-base: #FFFFFF;
$surface: rgba(255, 255, 255, 0.82);
$surface-solid: #FFFFFF;
$text-main: #1A2233;
$text-sub: #6A7A99;
$text-light: #9AA8C0;
$line-soft: rgba($brand-primary, 0.12);
$shadow-soft: 0 8rpx 20rpx rgba($brand-primary, 0.05);
.register-page {
min-height: 100vh;
position: relative;
background: #FFFFFF;
background-image:
radial-gradient(circle at 100% 0%, rgba($brand-primary, 0.15) 0%, transparent 40%),
radial-gradient(circle at 0% 100%, rgba($brand-primary, 0.05) 0%, transparent 40%);
overflow: hidden;
}
.register-page::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at 14% 18%, rgba($brand-primary, 0.05) 0, rgba($brand-primary, 0) 28%);
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 232rpx;
}
.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($brand-primary, 0.1);
}
.orb-b {
width: 220rpx;
height: 220rpx;
top: 520rpx;
left: -100rpx;
background: rgba($brand-primary, 0.06);
}
.ambient-grid {
position: absolute;
top: 140rpx;
right: 20rpx;
width: 200rpx;
height: 220rpx;
opacity: 0.24;
background-image:
linear-gradient(rgba($brand-primary, 0.12) 1px, transparent 1px),
linear-gradient(90deg, rgba($brand-primary, 0.12) 1px, transparent 1px);
background-size: 26rpx 26rpx;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 1), transparent);
pointer-events: none;
}
.register-intro {
position: relative;
margin-top: 34rpx;
padding: 8rpx 4rpx 0;
}
.intro-kicker-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.hero-kicker {
display: inline-flex;
align-items: center;
height: 48rpx;
padding: 0 20rpx;
border-radius: 999rpx;
background: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.12);
color: $brand-ink;
font-size: 22rpx;
letter-spacing: 1rpx;
}
.page-title {
margin-top: 22rpx;
font-size: 48rpx;
line-height: 1.24;
font-weight: 600;
letter-spacing: 0;
color: $text-main;
}
.draft-tip-bar {
display: inline-flex;
align-items: center;
margin-top: 22rpx;
padding: 12rpx 20rpx;
border-radius: 999rpx;
background: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.12);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.05);
}
.draft-tip-icon {
margin-right: 12rpx;
font-size: 24rpx;
color: $brand;
}
.draft-tip-text {
font-size: 22rpx;
font-weight: 500;
color: $brand-ink;
}
.progress-strip {
display: flex;
align-items: center;
padding: 14rpx;
margin: 28rpx 0 22rpx;
border-radius: 32rpx;
background: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: $shadow-soft;
}
.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: #FFFFFF;
color: $brand-primary;
border: 1rpx solid rgba($brand-primary, 0.16);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
}
.progress-node.is-completed {
background: rgba($brand-primary, 0.08);
color: $brand-primary;
}
.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: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: $shadow-soft;
}
.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: rgba($brand-primary, 0.1);
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: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.03);
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: $brand-primary;
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.15);
}
.input-placeholder {
color: #B2BECC;
}
.picker-input {
display: flex;
align-items: center;
justify-content: space-between;
}
.picker-text-wrap {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.picker-subtext {
margin-top: 6rpx;
font-size: 20rpx;
color: $text-light;
}
.picker-action-wrap {
display: inline-flex;
align-items: center;
margin-left: 20rpx;
flex-shrink: 0;
}
.picker-action-text {
margin-right: 10rpx;
font-size: 22rpx;
color: $brand-ink;
}
.picker-input-disabled {
background: rgba($brand-primary, 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($brand-primary, 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;
border: 1rpx solid rgba($brand-primary, 0.16);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
}
.role-active .role-tab-subtitle {
color: rgba($brand-primary, 0.7);
}
.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($brand-primary, 0.3);
background: rgba($brand-primary, 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($brand-primary, 0.08);
border-color: rgba($brand-primary, 0.28);
color: $brand-ink;
font-weight: 600;
}
.upload-box {
min-height: 260rpx;
padding: 40rpx 32rpx;
border-radius: 28rpx;
border: 1rpx dashed rgba($brand-primary, 0.24);
background:
linear-gradient(180deg, rgba($brand-primary, 0.03) 0%, #FFFFFF 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($brand-primary, 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: 1rpx solid rgba($brand-primary, 0.1);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.05);
}
.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($brand-primary, 0.05) 0%, #FFFFFF 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($brand-primary, 0.04);
border: 1rpx solid rgba($brand-primary, 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, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
border-color: transparent;
}
.bottom-btn-area {
position: fixed;
left: 20rpx;
right: 20rpx;
bottom: 14rpx;
bottom: calc(env(safe-area-inset-bottom) + 14rpx);
z-index: 20;
}
.action-dock {
padding: 0;
border-radius: 26rpx;
background: transparent;
border: 0;
box-shadow: none;
}
.step-action-row {
display: flex;
gap: 16rpx;
width: 100%;
}
.step-action-row.single-action .step-btn {
width: 100%;
}
.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 {
height: 96rpx;
flex: 1;
}
.step-btn {
min-width: 0;
}
.btn-secondary {
min-width: 196rpx;
background: #FFFFFF;
color: $brand-ink;
border: 1rpx solid rgba($brand-primary, 0.18);
}
.btn-primary {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #FFFFFF;
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
}
.submit-btn-text {
font-size: 28rpx;
font-weight: 600;
letter-spacing: 1rpx;
}
.step-btn-text {
font-size: 28rpx;
font-weight: 600;
}
.btn-disabled {
opacity: 0.56;
pointer-events: none;
}
.animation-fade-in {
opacity: 0;
animation: fadeIn 0.55s ease forwards;
}
.country-selector-popup {
padding: 28rpx 24rpx 32rpx;
background: #F8FAFF;
}
.country-selector-header {
padding: 8rpx 12rpx 0;
}
.country-selector-title {
font-size: 34rpx;
font-weight: 600;
color: $text-main;
}
.country-selector-subtitle {
margin-top: 10rpx;
font-size: 24rpx;
color: $text-sub;
}
.country-search-box {
display: flex;
align-items: center;
height: 92rpx;
margin-top: 24rpx;
padding: 0 24rpx;
border-radius: 24rpx;
background: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.08);
}
.country-search-icon {
font-size: 28rpx;
color: $text-light;
}
.country-search-input {
flex: 1;
min-width: 0;
height: 92rpx;
margin-left: 16rpx;
color: $text-main;
font-size: 28rpx;
}
.country-search-clear {
margin-left: 16rpx;
font-size: 24rpx;
color: $brand-ink;
}
.country-selector-list {
max-height: 760rpx;
margin-top: 20rpx;
}
.country-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx;
border-radius: 24rpx;
background: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 0.06);
}
.country-option + .country-option {
margin-top: 14rpx;
}
.country-option.is-active {
border-color: rgba($brand-primary, 0.22);
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.08);
}
.country-option-main {
flex: 1;
min-width: 0;
}
.country-option-name {
font-size: 28rpx;
font-weight: 600;
color: $text-main;
}
.country-option-meta {
margin-top: 8rpx;
font-size: 22rpx;
color: $text-sub;
}
.country-option-check {
margin-left: 20rpx;
font-size: 30rpx;
color: $brand;
}
.country-selector-empty {
padding: 72rpx 24rpx 96rpx;
font-size: 26rpx;
text-align: center;
color: $text-sub;
}
@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: #FFFFFF;
border: 1rpx solid rgba($brand-primary, 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, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
border-color: transparent;
}
</style>