Files
QSY-lvjuxcx/pkgAccount/register/register.vue
liangjian 6e721ccc7e refactor: 国际化文本迁移至全局 locales/messages.js
将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。

- 新增 utils/i18n.js 国际化工具函数
- 更新 store/$t.mixin.js 翻译混入逻辑
- 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
2026-07-24 13:43:41 +08:00

1897 lines
64 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="tn-text-bold tn-text-xl tn-color-black">{{ $tr('注册账号') }}</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">{{ $tr('注册并提交实名资料') }}</view>
<view class="tn-color-gray tn-text-sm page-desc">
{{ $tr('账号信息和实名认证资料需要一起填写完成,提交的是注册申请单,审批通过后才算注册成功。') }}
</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>
{{ $tr('账号信息') }}
</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.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 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>
{{ $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="form-item">
<view class="form-label">{{ $tr('姓名') }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="$tr('请输入真实姓名')" placeholder-class="input-placeholder" v-model="form.name" />
</view>
<view class="form-item">
<view class="form-label">{{ $tr('性别') }} <text class="required-mark">*</text></view>
<view class="tn-flex">
<view
v-for="(item, index) in genderOptions"
:key="item.key"
class="gender-btn tn-flex-1"
:class="[form.gender === Number(item.key) ? 'gender-active' : '', index > 0 ? 'tn-margin-left-sm' : '']"
@click="form.gender = Number(item.key)"
>
{{ item.value }}
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">{{ $tr('手机号') }} <text class="required-mark">*</text></view>
<input class="form-input" type="number" :placeholder="$tr('请输入手机号')" placeholder-class="input-placeholder" v-model="form.phone" />
</view>
<view class="form-item" style="margin-bottom: 0;">
<view class="form-label">{{ $tr('国籍') }} <text class="required-mark">*</text></view>
<view v-if="isDomesticIdentity" class="form-input picker-input picker-input-disabled">
<text class="text-black">{{ form.country || $tr('中国') }}</text>
<text class="picker-fixed-text">{{ $tr('中国籍固定') }}</text>
</view>
<picker v-else mode="selector" :range="countryRange" @change="onCountryChange">
<view class="form-input picker-input">
<text :class="form.country ? 'text-black' : 'text-gray'">{{ form.country || $tr('请选择国籍') }}</text>
<text class="tn-icon-right 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>
{{ $tr('认证档案') }}
</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.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="tn-margin-top-sm upload-text">{{ $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 tn-margin-top-xs">{{ $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 tn-color-gray"></text>
</view>
</picker>
</view>
<template v-if="form.identityType === 'DomesticStudents'">
<view class="form-item">
<view class="form-label">{{ $tr('身份证号') }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="$tr('请输入身份证号')" placeholder-class="input-placeholder" v-model="form.idCard" />
</view>
<view class="form-item">
<view class="form-label">{{ $tr('身份证正面附件') }} <text class="required-mark">*</text></view>
<view v-if="!form.idCardFrontAttachment.serverValue" class="upload-box" @click="uploadAttachment('idCardFrontAttachment')">
<view class="upload-placeholder">
<view class="upload-icon-wrap"><text class="tn-icon-camera-fill"></text></view>
<view class="tn-margin-top-sm upload-text">{{ $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 tn-margin-top-xs">{{ $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="tn-margin-top-sm upload-text">{{ $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 tn-margin-top-xs">{{ $tr('已上传图片,可点击查看大图') }}</view>
</view>
<view class="attachment-action-row">
<view class="attachment-action-btn" @click.stop="previewAttachment('idCardBackAttachment')">
{{ $tr('查看图片') }}
</view>
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('idCardBackAttachment')">
{{ $tr('重新上传') }}
</view>
</view>
</view>
</view>
</template>
<template v-else>
<view class="form-item">
<view class="form-label">{{ foreignDocumentNumberLabel }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="foreignDocumentNumberPlaceholder" placeholder-class="input-placeholder" v-model="form.passportNumber" />
</view>
<view class="form-item">
<view class="form-label">{{ foreignDocumentExpiryLabel }} <text class="required-mark">*</text></view>
<picker mode="date" @change="onPassportDateChange">
<view class="form-input picker-input">
<text :class="form.passportExpiry ? 'text-black' : 'text-gray'">{{ form.passportExpiry || $tr('请选择日期') }}</text>
<text class="tn-icon-calendar tn-color-gray"></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 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">{{ foreignDocumentUploadText }}</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) ? $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 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>
{{ $tr('偏好信息') }}
</view>
<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 tn-color-gray"></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 tn-color-gray"></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 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 ? $tr('提交中...') : $tr('提交注册申请') }}</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">{{ $tr('查看注册记录') }}</view>
<view class="secondary-entry-desc tn-margin-top-xs">{{ $tr('如果当前微信账号已提交过申请,可直接查看历史记录和审批状态') }}</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">{{ $tr('已有账号?') }}</view>
<view class="tn-color-black tn-text-bold tn-text-sm tn-margin-left-xs login-link" @click="goToLogin">{{ $tr('去登录') }}</view>
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<view v-if="showPrivacyPopup" class="privacy-mask">
<view class="privacy-dialog">
<view class="privacy-title">{{ $tr('隐私授权提示') }}</view>
<view class="privacy-content">
{{ $tr('上传证件图片前,请先阅读并同意') }}
<text class="privacy-link" @click="openPrivacyContract">{{ $tr('《用户隐私保护指引》') }}</text>
{{ $tr(',同意后才能继续选择图片。') }}
</view>
<view class="privacy-action-row">
<view class="privacy-action-btn" @click="handlePrivacyDisagree">{{ $tr('暂不授权') }}</view>
<button
id="register-privacy-agree-btn"
class="privacy-action-btn is-primary"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
>
{{ $tr('同意并继续') }}
</button>
</view>
</view>
</view>
<!-- #endif -->
</view>
</template>
<script>
import {
clearFailedRegistrationDraft,
clearLoginSession,
getFailedRegistrationDraft,
saveFailedRegistrationDraft
} from '../../utils/app-state'
import { getCountryList, submitRegistrationApplication, sysdictPage, uploadCommonFile, registrationMyorder } from '@/api/index.js'
import { addFilePrefix } from '@/utils/common.js'
import { bindPrivacyAuthorizationListener, openWechatPrivacyContract, unbindPrivacyAuthorizationListener } from '@/utils/privacy.js'
import { fetchEnums, getEnumOptions, getGenderLabel } from '../../utils/enum-store'
import { resolveRegistrationApplicationCode } from '../../utils/flow'
function createAttachmentState() {
return {
name: '',
serverValue: '',
localPath: ''
}
}
function createDefaultForm() {
return {
identityType: 'ForeignStudents',
account: '',
password: '',
email: '',
emailVerifyCode: '',
name: '',
gender: 1,
phone: '',
country: '',
idCardType: 2,
idCard: '',
workUnit: '',
hasHangzhouProperty: null,
dietaryHabit: '',
hobby: '',
passportNumber: '',
passportExpiry: '',
school: '',
religion: '',
admissionNoticeAttachment: createAttachmentState(),
passportAttachment: createAttachmentState(),
idCardFrontAttachment: createAttachmentState(),
idCardBackAttachment: createAttachmentState()
}
}
function normalizeAttachmentDraft(attachment) {
const source = attachment && typeof attachment === 'object' ? attachment : {}
return {
...createAttachmentState(),
name: source.name || '',
serverValue: source.serverValue || '',
localPath: source.localPath || ''
}
}
function normalizeRegistrationDraftForm(draft) {
const draftSource = draft && typeof draft === 'object' ? draft : {}
const source = draftSource.form && typeof draftSource.form === 'object' ? draftSource.form : draftSource
return {
...createDefaultForm(),
...source,
gender: source.gender === 0 || source.gender === 1 ? Number(source.gender) : 1,
idCardType: Number(source.idCardType || source.IdCardType || 0) || 2,
hasHangzhouProperty: typeof source.hasHangzhouProperty === 'boolean' ? source.hasHangzhouProperty : null,
admissionNoticeAttachment: normalizeAttachmentDraft(source.admissionNoticeAttachment),
passportAttachment: normalizeAttachmentDraft(source.passportAttachment),
idCardFrontAttachment: normalizeAttachmentDraft(source.idCardFrontAttachment),
idCardBackAttachment: normalizeAttachmentDraft(source.idCardBackAttachment)
}
}
export default {
data() {
return {
navBgOpacity: 0,
isSubmitting: false,
dictLoading: false,
hasPromptedFailedDraft: false,
draftPersistTimer: null,
draftTipText: '',
lastForeignCountry: '',
lastForeignIdCardType: 2,
showPrivacyPopup: false,
resolvePrivacyAuthorization: null,
form: createDefaultForm(),
rawCountryList: [],
rawHobbyList: [],
rawDietaryHabitList: [],
rawReligionList: [],
rawIdCardTypeList: [],
countryOptions: [],
hobbyOptions: [],
dietaryHabitOptions: [],
religionOptions: [],
idCardTypeOptions: []
}
},
computed: {
language() {
return this.$getLanguage()
},
navBackgroundColor() {
return `rgba(255, 255, 255, ${this.navBgOpacity})`
},
hobbyRange() {
return this.hobbyOptions.length ? this.hobbyOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
countryRange() {
return this.countryOptions.length ? this.countryOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
idCardTypeRange() {
return this.idCardTypeOptions.length ? this.idCardTypeOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
dietaryHabitRange() {
return this.dietaryHabitOptions.length ? this.dietaryHabitOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
isDomesticIdentity() {
return this.form.identityType === 'DomesticStudents'
},
religionRange() {
return this.religionOptions.length ? this.religionOptions.map(item => item.value) : [this.$tr('当前暂无可选项')]
},
selectedIdCardTypeLabel() {
const matched = this.idCardTypeOptions.find(item => String(item.key) === String(this.form.idCardType))
return matched ? matched.value : ''
},
isPassportType() {
return String(this.form.idCardType) === '2'
},
foreignDocumentNumberLabel() {
return this.isPassportType ? this.$tr('护照号') : this.$tr('证件号')
},
foreignDocumentNumberPlaceholder() {
return this.isPassportType ? this.$tr('请输入护照号') : this.$tr('请输入证件号')
},
foreignDocumentExpiryLabel() {
return this.isPassportType ? this.$tr('护照到期时间') : this.$tr('证件到期时间')
},
foreignDocumentAttachmentLabel() {
return this.isPassportType ? this.$tr('护照附件') : this.$tr('证件附件')
},
foreignDocumentUploadText() {
return this.isPassportType ? this.$tr('点击上传护照图片') : this.$tr('点击上传证件图片')
},
foreignDocumentImageOnlyTip() {
return this.isPassportType ? this.$tr('护照附件仅支持图片,请重新上传') : this.$tr('证件附件仅支持图片,请重新上传')
},
genderOptions() {
return [
{ key: 1, value: this.$tr('男') },
{ key: 0, value: this.$tr('女') }
]
}
},
watch: {
language() {
this.rebuildLocalizedOptions()
this.draftTipText = this.$tr('未完成内容会自动保存,可随时返回继续填写')
},
form: {
deep: true,
handler() {
this.scheduleDraftPersist()
}
}
},
async onLoad(options) {
this.draftTipText = this.$tr('未完成内容会自动保存,可随时返回继续填写')
this.initPrivacyAuthorization()
const bypass = options && options.bypass === '1'
const hasRedirected = await this.checkPendingRegistration(bypass)
if (hasRedirected) return
await Promise.all([
this.loadDictOptions(),
this.loadCountryOptions()
])
this.tryRestoreFailedDraft()
},
onHide() {
this.persistRegistrationDraft()
},
onUnload() {
this.persistRegistrationDraft()
this.clearDraftPersistTimer()
this.resolvePrivacyAuthorization = null
this.releasePrivacyAuthorization()
},
onPageScroll(e) {
let opacity = e.scrollTop / 80
if (opacity > 1) {
opacity = 1
} else if (opacity < 0) {
opacity = 0
}
this.navBgOpacity = opacity
},
methods: {
isChinaCountry(value = '') {
const text = String(value || '').trim()
return text === '中国' || text === 'China' || text === this.$tr('中国')
},
async checkPendingRegistration(bypassRedirect = false) {
uni.showLoading({ title: this.$tr('加载中...') })
try {
const providerValue = await this.getWechatCode()
if (!providerValue) return false
const response = await registrationMyorder({ providerValue }, this.language)
const rawLatest = response && response.data && typeof response.data === 'object' ? response.data : response
const latest = rawLatest.data !== undefined ? rawLatest.data : rawLatest
if (latest && typeof latest === 'object' && Object.keys(latest).length > 0) {
if (!bypassRedirect) {
uni.redirectTo({ url: '/pkgAccount/auth_result/auth_result' })
return true
}
}
} catch (e) {
console.error('check pending registration failed', e)
} finally {
uni.hideLoading()
}
return false
},
goToLogin() {
uni.navigateBack()
},
goToRegistrationRecords() {
uni.navigateTo({
url: '/pkgAccount/registration_records/registration_records'
})
},
tryRestoreFailedDraft() {
if (this.hasPromptedFailedDraft) {
return
}
const cachedDraft = getFailedRegistrationDraft()
if (!cachedDraft || typeof cachedDraft !== 'object') {
return
}
this.hasPromptedFailedDraft = true
uni.showModal({
title: this.$tr('发现未完成的数据'),
content: this.$tr('是否填入上次未完成或提交失败时缓存的注册信息?'),
confirmText: this.$tr('填入'),
cancelText: this.$tr('不用'),
success: ({ confirm }) => {
if (confirm) {
this.applyFailedDraft(cachedDraft)
return
}
clearFailedRegistrationDraft()
}
})
},
applyFailedDraft(draft) {
this.form = normalizeRegistrationDraftForm(draft)
if (!this.isDomesticIdentity && this.form.country && !this.isChinaCountry(this.form.country)) {
this.lastForeignCountry = this.form.country
}
if (!this.isDomesticIdentity && String(this.form.idCardType || '') !== '1') {
this.lastForeignIdCardType = Number(this.form.idCardType) || 2
}
this.applyIdentityDocumentTypeRule()
this.applyIdentityCountryRule()
uni.showToast({
title: this.$tr('已填入上次失败数据'),
icon: 'success'
})
},
buildFailedDraft() {
return normalizeRegistrationDraftForm(this.form)
},
scheduleDraftPersist() {
if (this.isSubmitting) {
return
}
this.clearDraftPersistTimer()
this.draftPersistTimer = setTimeout(() => {
this.persistRegistrationDraft(true)
}, 600)
},
clearDraftPersistTimer() {
if (!this.draftPersistTimer) {
return
}
clearTimeout(this.draftPersistTimer)
this.draftPersistTimer = null
},
hasDraftContent() {
const attachmentFields = [
'admissionNoticeAttachment',
'passportAttachment',
'idCardFrontAttachment',
'idCardBackAttachment'
]
const textFields = [
'account',
'password',
'email',
'emailVerifyCode',
'name',
'phone',
'country',
'idCard',
'workUnit',
'dietaryHabit',
'hobby',
'passportNumber',
'passportExpiry',
'school',
'religion'
]
if (this.form.identityType !== 'ForeignStudents') {
return true
}
if (this.form.gender !== null || this.form.hasHangzhouProperty !== null) {
return true
}
if (textFields.some(field => String(this.form[field] || '').trim())) {
return true
}
return attachmentFields.some((field) => {
const attachment = this.form[field]
return attachment && (attachment.serverValue || attachment.localPath || attachment.name)
})
},
persistRegistrationDraft(isSilent = false) {
if (this.isSubmitting) {
return
}
this.clearDraftPersistTimer()
if (!this.hasDraftContent()) {
clearFailedRegistrationDraft()
this.draftTipText = this.$tr('未完成内容会自动保存,可随时返回继续填写')
return
}
saveFailedRegistrationDraft({
form: this.buildFailedDraft(),
updatedAt: Date.now()
})
if (!isSilent) {
this.draftTipText = this.$tr('当前填写内容已自动保存')
}
},
switchIdentityType(type) {
if (this.form.identityType === type) return
if (type === 'DomesticStudents' && this.form.country) {
this.lastForeignCountry = this.form.country
}
this.form.identityType = type
this.applyIdentityDocumentTypeRule()
this.applyIdentityCountryRule()
},
onPassportDateChange(e) {
this.form.passportExpiry = e.detail.value
},
loadCountryOptions() {
return getCountryList(this.language)
.then((countryRes) => {
const rawList = this.extractResponseList(countryRes)
this.rawCountryList = rawList
this.countryOptions = this.normalizeCountryList(rawList)
this.applyIdentityCountryRule()
})
.catch((error) => {
console.error('load country options failed', error)
uni.showToast({
title: this.$tr('国家列表加载失败'),
icon: 'none'
})
})
},
loadDictOptions() {
this.dictLoading = true
return Promise.all([
sysdictPage({ Type: 'UserHobbiesAndInterestsTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
sysdictPage({ Type: 'UserDietaryHabitsTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
sysdictPage({ Type: 'UserReligiousBeliefTag', PageIndex: 1, PageSize: 100 }, this.$getLanguage()),
fetchEnums(false, this.$getLanguage())
])
.then(([hobbyRes, dietaryRes, religionRes]) => {
this.rawHobbyList = this.extractResponseList(hobbyRes)
this.rawDietaryHabitList = this.extractResponseList(dietaryRes)
this.rawReligionList = this.extractResponseList(religionRes)
this.rawIdCardTypeList = getEnumOptions('IdCardTypeEnum', this.$getLanguage())
this.hobbyOptions = this.normalizeDictList(this.rawHobbyList)
this.dietaryHabitOptions = this.normalizeDictList(this.rawDietaryHabitList)
this.religionOptions = this.normalizeDictList(this.rawReligionList)
this.idCardTypeOptions = this.normalizeDictList(this.rawIdCardTypeList)
this.applyIdentityDocumentTypeRule()
})
.catch((error) => {
console.error('load dict options failed', error)
uni.showToast({
title: this.$tr('字典加载失败,稍后可重试'),
icon: 'none'
})
})
.finally(() => {
this.dictLoading = false
})
},
extractResponseList(response) {
const rawList = response && response.data
? (response.data.data || response.data)
: (response && response.data ? response.data : response)
return Array.isArray(rawList) ? rawList : []
},
getOptionTextCandidates(item = {}) {
return [
item.value,
item.label,
item.description,
item.name,
item.cname,
item.nameZh,
item.labelZh,
item.valueZh,
item.descriptionZh,
item.ename,
item.nameEn,
item.labelEn,
item.valueEn,
item.descriptionEn,
item.code
].map(value => String(value || '').trim()).filter(Boolean)
},
getLocalizedOptionText(item = {}) {
const isEn = this.language === 'en'
const preferred = isEn
? [item.descriptionEn, item.ename, item.nameEn, item.labelEn, item.valueEn, item.description, item.value, item.label, item.name, item.cname, item.code]
: [item.description, item.cname, item.nameZh, item.labelZh, item.valueZh, item.value, item.label, item.name, item.code]
const matched = preferred.map(value => String(value || '').trim()).find(Boolean)
return matched || ''
},
normalizeDictList(rawList = []) {
const list = Array.isArray(rawList) ? rawList : []
return list
.map(item => ({
key: item.key || item.code || '',
value: this.getLocalizedOptionText(item)
}))
.filter(item => item.value)
},
normalizeCountryList(rawList = []) {
const list = Array.isArray(rawList) ? rawList : []
return list
.map(item => ({
key: item.code || item.key || item.cname || '',
value: this.getLocalizedOptionText(item)
}))
.filter(item => item.value)
},
remapSelectedValue(currentValue, rawList = []) {
const normalizedCurrent = String(currentValue || '').trim()
if (!normalizedCurrent) {
return ''
}
const matched = (Array.isArray(rawList) ? rawList : []).find((item) => {
return this.getOptionTextCandidates(item).includes(normalizedCurrent)
})
return matched ? this.getLocalizedOptionText(matched) : normalizedCurrent
},
rebuildLocalizedOptions() {
this.countryOptions = this.normalizeCountryList(this.rawCountryList)
this.hobbyOptions = this.normalizeDictList(this.rawHobbyList)
this.dietaryHabitOptions = this.normalizeDictList(this.rawDietaryHabitList)
this.religionOptions = this.normalizeDictList(this.rawReligionList)
this.idCardTypeOptions = this.normalizeDictList(this.rawIdCardTypeList)
this.form.country = this.remapSelectedValue(this.form.country, this.rawCountryList)
this.lastForeignCountry = this.remapSelectedValue(this.lastForeignCountry, this.rawCountryList)
this.form.hobby = this.remapSelectedValue(this.form.hobby, this.rawHobbyList)
this.form.dietaryHabit = this.remapSelectedValue(this.form.dietaryHabit, this.rawDietaryHabitList)
this.form.religion = this.remapSelectedValue(this.form.religion, this.rawReligionList)
this.applyIdentityDocumentTypeRule()
this.applyIdentityCountryRule()
},
applyIdentityDocumentTypeRule() {
if (this.isDomesticIdentity) {
if (String(this.form.idCardType || '') !== '1' && String(this.form.idCardType || '')) {
this.lastForeignIdCardType = Number(this.form.idCardType) || 2
}
this.form.idCardType = 1
return
}
if (!this.form.idCardType || String(this.form.idCardType) === '1') {
this.form.idCardType = this.lastForeignIdCardType || 2
}
},
getChinaCountryOption() {
return this.countryOptions.find(item => String(item.key || '').toUpperCase() === 'CHN' || this.isChinaCountry(item.value)) || null
},
applyIdentityCountryRule() {
if (this.isDomesticIdentity) {
const chinaOption = this.getChinaCountryOption()
this.form.country = chinaOption ? chinaOption.value : this.$tr('中国')
return
}
if (this.isChinaCountry(this.form.country)) {
this.form.country = this.lastForeignCountry || ''
}
},
onDictSelect(field, event) {
const map = {
dietaryHabit: this.dietaryHabitOptions,
hobby: this.hobbyOptions,
religion: this.religionOptions
}
const options = map[field] || []
if (!options.length) {
uni.showToast({
title: this.$tr('当前暂无可选项'),
icon: 'none'
})
return
}
const selected = options[event.detail.value]
this.form[field] = selected ? selected.value : ''
},
onIdCardTypeChange(event) {
if (!this.idCardTypeOptions.length) {
uni.showToast({
title: this.$tr('当前暂无可选项'),
icon: 'none'
})
return
}
const selected = this.idCardTypeOptions[event.detail.value]
const nextValue = selected ? Number(selected.key) : 0
this.form.idCardType = nextValue || this.form.idCardType
if (!this.isDomesticIdentity && nextValue && nextValue !== 1) {
this.lastForeignIdCardType = nextValue
}
},
onCountryChange(event) {
if (this.isDomesticIdentity) return
if (!this.countryOptions.length) {
uni.showToast({
title: this.$tr('当前暂无可选国家'),
icon: 'none'
})
return
}
const selected = this.countryOptions[event.detail.value]
this.form.country = selected ? selected.value : ''
this.lastForeignCountry = this.form.country || ''
},
initPrivacyAuthorization() {
// #ifdef MP-WEIXIN
if (typeof wx === 'undefined' || !wx.onNeedPrivacyAuthorization) {
return
}
this.handlePrivacyAuthorizationRequest = (resolve) => {
this.resolvePrivacyAuthorization = resolve
this.showPrivacyPopup = true
}
bindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
// #endif
},
releasePrivacyAuthorization() {
// #ifdef MP-WEIXIN
if (!this.handlePrivacyAuthorizationRequest) {
return
}
unbindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
this.handlePrivacyAuthorizationRequest = null
this.showPrivacyPopup = false
// #endif
},
ensurePrivacyAuthorized() {
// #ifdef MP-WEIXIN
if (typeof wx === 'undefined' || !wx.requirePrivacyAuthorize) {
return Promise.resolve()
}
return new Promise((resolve, reject) => {
const triggerPrivacyAuthorize = () => {
wx.requirePrivacyAuthorize({
success: () => {
this.showPrivacyPopup = false
resolve()
},
fail: (error) => {
reject({
...error,
privacyDenied: true
})
}
})
}
if (!wx.getPrivacySetting) {
triggerPrivacyAuthorize()
return
}
wx.getPrivacySetting({
success: (res) => {
if (!res.needAuthorization) {
resolve()
return
}
triggerPrivacyAuthorize()
},
fail: () => {
triggerPrivacyAuthorize()
}
})
})
// #endif
// #ifndef MP-WEIXIN
return Promise.resolve()
// #endif
},
openPrivacyContract() {
// #ifdef MP-WEIXIN
openWechatPrivacyContract().catch((error) => {
console.error('open privacy contract failed', error)
})
// #endif
},
handleAgreePrivacyAuthorization() {
this.showPrivacyPopup = false
if (!this.resolvePrivacyAuthorization) {
return
}
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
this.resolvePrivacyAuthorization = null
resolvePrivacyAuthorization({
buttonId: 'register-privacy-agree-btn',
event: 'agree'
})
},
handlePrivacyDisagree() {
this.showPrivacyPopup = false
if (!this.resolvePrivacyAuthorization) {
return
}
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
this.resolvePrivacyAuthorization = null
resolvePrivacyAuthorization({
event: 'disagree'
})
},
uploadAttachment(field) {
return this.ensurePrivacyAuthorized()
.then(() => this.chooseImageFile())
.then((file) => {
if (!file || !file.path) return
uni.showLoading({
title: this.$tr('上传中...')
})
return uploadCommonFile(file.path).then((uploadResult) => {
const serverValue = this.resolveUploadedValue(uploadResult, file.path)
this.form[field] = {
name: file.name || this.getDefaultAttachmentName(field, file.path),
serverValue,
localPath: file.path
}
uni.showToast({
title: this.$tr('上传成功'),
icon: 'success'
})
})
})
.catch((error) => {
if (error && error.cancel) return
if (error && error.privacyDenied) {
uni.showToast({
title: this.$tr('未完成隐私授权,暂时无法选择图片'),
icon: 'none'
})
return
}
console.error('upload attachment failed', error)
uni.showToast({
title: this.$tr('上传失败,请重试'),
icon: 'none'
})
})
.finally(() => {
uni.hideLoading()
})
},
chooseImageFile() {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
success: (res) => {
const filePath = res.tempFilePaths && res.tempFilePaths[0]
const tempFile = res.tempFiles && res.tempFiles[0]
resolve({
path: filePath,
name: tempFile && tempFile.name ? tempFile.name : this.getFileNameFromPath(filePath)
})
},
fail: (error) => {
if (error && /cancel/i.test(JSON.stringify(error))) {
reject({ cancel: true })
return
}
reject(error)
}
})
})
},
previewAttachment(field) {
const attachment = this.form[field]
const previewUrl = this.getAttachmentPreviewUrl(attachment)
if (!previewUrl || !this.isImageAttachment(attachment)) {
uni.showToast({
title: this.$tr('当前附件不是图片'),
icon: 'none'
})
return
}
uni.previewImage({
urls: [previewUrl],
current: previewUrl
})
},
getAttachmentPreviewUrl(attachment) {
if (!attachment) return ''
const candidates = [attachment.serverValue, attachment.localPath]
for (const item of candidates) {
const value = String(item || '').trim()
if (!value) continue
if (this.isLocalFileUrl(value)) {
return value
}
return addFilePrefix(value)
}
return ''
},
isImageAttachment(attachment) {
return ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(this.getAttachmentExtension(attachment))
},
isLocalFileUrl(url = '') {
return /^(wxfile:\/\/|file:\/\/|blob:|data:image|_doc\/|\/tmp\/)/i.test(String(url || '').trim())
},
getAttachmentExtension(attachment) {
const candidates = [
attachment && attachment.name,
attachment && attachment.serverValue,
attachment && attachment.localPath
]
const matched = candidates.find(item => String(item || '').trim())
if (!matched) return ''
const cleanValue = String(matched).split('?')[0].split('#')[0]
const segments = cleanValue.split('.')
return segments.length > 1 ? segments.pop().toLowerCase() : ''
},
submitForm() {
if (this.isSubmitting) return
const errorMessage = this.validateForm()
if (errorMessage) {
uni.showToast({
title: errorMessage,
icon: 'none'
})
return
}
this.isSubmitting = true
uni.showLoading({
title: this.$tr('提交中...')
})
return this.getWechatCode().then((providerValue) => {
const submitPayload = this.buildSubmitPayload(providerValue)
return submitRegistrationApplication(submitPayload)
.then((response) => {
const registrationCode = resolveRegistrationApplicationCode(response)
clearLoginSession()
clearFailedRegistrationDraft()
uni.hideLoading()
uni.showToast({
title: this.$tr('申请已提交,待审批'),
icon: 'success'
})
setTimeout(() => {
uni.redirectTo({
url: '/pkgAccount/auth_result/auth_result'
})
}, 800)
})
.catch((error) => {
console.error('submit registration application failed', error)
saveFailedRegistrationDraft(this.buildFailedDraft())
uni.hideLoading()
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('提交失败,请稍后重试'),
icon: 'none'
})
})
.finally(() => {
this.isSubmitting = false
})
})
},
getWechatCode() {
return new Promise((resolve) => {
// #ifdef MP-WEIXIN
uni.login({
provider: 'weixin',
success: (res) => {
resolve(res.code || '')
},
fail: (err) => {
console.error('getWechatCode failed', err)
resolve('')
}
})
// #endif
// #ifndef MP-WEIXIN
resolve('')
// #endif
})
},
resolveUploadedValue(uploadResult, fallback) {
const candidates = [
uploadResult && uploadResult.data && uploadResult.data.url,
uploadResult && uploadResult.data && uploadResult.data.fileUrl,
uploadResult && uploadResult.data && uploadResult.data.path,
uploadResult && uploadResult.data && uploadResult.data.filePath,
uploadResult && uploadResult.url,
uploadResult && uploadResult.fileUrl,
uploadResult && uploadResult.path,
uploadResult && uploadResult.filePath,
typeof (uploadResult && uploadResult.data) === 'string' ? uploadResult.data : '',
typeof uploadResult === 'string' ? uploadResult : ''
]
return candidates.find(item => typeof item === 'string' && item) || fallback
},
getDefaultAttachmentName(field, filePath) {
if (field === 'passportAttachment') return 'passport-attachment'
if (field === 'idCardFrontAttachment') return 'id-card-front'
if (field === 'idCardBackAttachment') return 'id-card-back'
if (field === 'admissionNoticeAttachment') return 'admission-notice-attachment'
return this.getFileNameFromPath(filePath)
},
getFileNameFromPath(filePath = '') {
const normalizedPath = String(filePath || '').replace(/\\/g, '/')
return normalizedPath.split('/').pop() || 'attachment'
},
validateForm() {
if (!this.form.account || !this.form.password || !this.form.email || !this.form.name) {
return this.$tr('请先填写账号、密码、邮箱和姓名')
}
if (this.form.password.length < 6) {
return this.$tr('登录密码至少 6 位')
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.form.email)) {
return this.$tr('请输入正确的邮箱地址')
}
if (this.form.gender === null || !this.form.phone || !this.form.country) {
return this.$tr('请完善性别、手机号和国籍')
}
if (!/^1\d{10}$/.test(this.form.phone)) {
return this.$tr('请输入正确的手机号')
}
if (!this.form.workUnit || this.form.hasHangzhouProperty === null) {
return this.$tr('请完善工作单位和杭州房产情况')
}
if (!this.form.idCardType) {
return this.$tr('请选择证件类型')
}
if (this.form.identityType === 'DomesticStudents') {
if (!this.form.idCard) {
return this.$tr('请填写身份证号')
}
if (!this.form.idCardFrontAttachment.serverValue || !this.form.idCardBackAttachment.serverValue) {
return this.$tr('请上传身份证正反面附件')
}
return ''
}
if (!this.form.passportNumber || !this.form.passportExpiry) {
return this.isPassportType ? this.$tr('请填写护照号和护照到期时间') : this.$tr('请填写证件号和证件到期时间')
}
if (!this.form.passportAttachment.serverValue) {
return this.isPassportType ? this.$tr('请上传护照图片') : this.$tr('请上传证件图片')
}
if (!this.isImageAttachment(this.form.passportAttachment)) {
return this.isPassportType ? this.$tr('护照附件仅支持图片格式') : this.$tr('证件附件仅支持图片格式')
}
return ''
},
buildSingleTag(text, options) {
if (!text) return []
const matched = (options || []).find(item => item.value === text)
if (matched) {
return [{ key: matched.key, value: matched.value }]
}
return [{ key: text, value: text }]
},
buildAuthProfile() {
return {
identityType: this.form.identityType,
account: this.form.account,
email: this.form.email,
emailVerifyCode: this.form.emailVerifyCode,
roleNo: this.form.identityType,
name: this.form.name,
gender: this.form.gender,
genderText: getGenderLabel(this.form.gender, this.form.gender === 1 ? this.$tr('男') : this.form.gender === 0 ? this.$tr('女') : ''),
phone: this.form.phone,
country: this.form.country,
idCardType: Number(this.form.idCardType) || null,
idCardTypeText: this.selectedIdCardTypeLabel || '',
idCard: this.form.identityType === 'DomesticStudents' ? this.form.idCard : '',
workUnit: this.form.workUnit,
hasHangzhouProperty: this.form.hasHangzhouProperty,
dietaryHabit: this.form.dietaryHabit,
dietaryHabitTags: this.buildSingleTag(this.form.dietaryHabit, this.dietaryHabitOptions),
hobby: this.form.hobby,
hobbyTags: this.buildSingleTag(this.form.hobby, this.hobbyOptions),
passportNumber: this.form.identityType === 'ForeignStudents' ? this.form.passportNumber : '',
passportExpiry: this.form.identityType === 'ForeignStudents' && this.form.passportExpiry
? `${this.form.passportExpiry}T00:00:00`
: '',
school: this.form.identityType === 'ForeignStudents' ? this.form.school : '',
religion: this.form.religion,
religionTags: this.buildSingleTag(this.form.religion, this.religionOptions),
admissionNoticeAttachment: this.form.admissionNoticeAttachment.serverValue || '',
passportAttachment: this.form.identityType === 'ForeignStudents' ? this.form.passportAttachment.serverValue : '',
idCardFrontAttachment: this.form.identityType === 'DomesticStudents' ? this.form.idCardFrontAttachment.serverValue : '',
idCardBackAttachment: this.form.identityType === 'DomesticStudents' ? this.form.idCardBackAttachment.serverValue : ''
}
},
buildSubmitPayload(providerValue) {
const profile = this.buildAuthProfile()
return {
providerValue: providerValue || null,
account: profile.account,
password: this.form.password,
email: profile.email,
emailVerifyCode: profile.emailVerifyCode || null,
roleNo: profile.roleNo,
name: profile.name,
gender: profile.gender,
phone: profile.phone,
country: profile.country,
idCardType: profile.idCardType || null,
IdCardType: profile.idCardType || null,
idCard: profile.idCard || null,
workUnit: profile.workUnit,
hasHangzhouProperty: profile.hasHangzhouProperty,
dietaryHabitTags: profile.dietaryHabitTags.length ? profile.dietaryHabitTags : null,
hobbyTags: profile.hobbyTags.length ? profile.hobbyTags : null,
passportNumber: profile.passportNumber || null,
passportExpiry: profile.passportExpiry || null,
school: profile.school || null,
religionTags: profile.religionTags.length ? profile.religionTags : null,
admissionNoticeAttachment: profile.admissionNoticeAttachment || null,
passportAttachment: profile.passportAttachment || null,
idCardFrontAttachment: profile.idCardFrontAttachment || null,
idCardBackAttachment: profile.idCardBackAttachment || null
}
},
}
}
</script>
<style scoped lang="scss">
.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-tab-subtitle {
margin-top: 6rpx;
font-size: 20rpx;
font-weight: 500;
line-height: 1.2;
color: #8A99B3;
}
.role-active {
background-color: #FFFFFF;
color: $brand-primary;
box-shadow: 0 6rpx 20rpx rgba($brand-primary, 0.15);
}
.role-active .role-tab-subtitle {
color: rgba($brand-primary, 0.82);
}
/* 性别按钮 */
.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;
}
.privacy-mask {
position: fixed;
inset: 0;
z-index: 999;
background: rgba(17, 24, 39, 0.56);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.privacy-dialog {
width: 100%;
max-width: 640rpx;
background: #FFFFFF;
border-radius: 28rpx;
padding: 40rpx 32rpx 32rpx;
box-sizing: border-box;
box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, 0.16);
}
.privacy-title {
font-size: 34rpx;
font-weight: 700;
color: #1A2233;
text-align: center;
}
.privacy-content {
margin-top: 24rpx;
font-size: 28rpx;
line-height: 1.7;
color: #5C6B8A;
}
.privacy-link {
color: $brand-primary;
font-weight: 700;
}
.privacy-action-row {
display: flex;
margin-top: 32rpx;
}
.privacy-action-btn {
flex: 1;
height: 88rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
color: $brand-primary;
background: rgba($brand-primary, 0.08);
border: 1rpx solid rgba($brand-primary, 0.12);
box-sizing: border-box;
}
.privacy-action-btn + .privacy-action-btn {
margin-left: 20rpx;
}
button.privacy-action-btn {
padding: 0;
line-height: 88rpx;
}
button.privacy-action-btn::after {
border: none;
}
.privacy-action-btn.is-primary {
color: #FFFFFF;
background: linear-gradient(135deg, rgba($brand-primary, 0.92) 0%, $brand-primary 100%);
border-color: transparent;
}
</style>