feat: 新增工单中心、房间巡查、失物招领等业务模块

- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面
- 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面
- 预约看样流程新增房型/楼栋/楼层选择器组件
- 新增工具模块: booking-entry、privacy、reservation-flow
- api/index.js 补充楼层列表、看样提交等接口
- locales/messages.js 补充多语言文案
- manifest.json 开启微信小程序隐私检查
- 完善登录、注册、申请详情、账单等相关页面交互
This commit is contained in:
liangjian
2026-07-24 09:35:46 +08:00
parent 9fea127de1
commit 445bd850f4
36 changed files with 9281 additions and 340 deletions

View File

@@ -53,10 +53,12 @@
<view class="role-switch-wrap">
<view class="role-switch tn-flex">
<view class="role-tab tn-flex-1" :class="form.identityType === 'ForeignStudents' ? 'role-active' : ''" @click="switchIdentityType('ForeignStudents')">
{{ t('foreignIdentity') }}
<view>{{ t('foreignIdentity') }}</view>
<view v-if="language !== 'en'" class="role-tab-subtitle">{{ t('foreignIdentityHint') }}</view>
</view>
<view class="role-tab tn-flex-1" :class="form.identityType === 'DomesticStudents' ? 'role-active' : ''" @click="switchIdentityType('DomesticStudents')">
{{ t('domesticIdentity') }}
<view>{{ t('domesticIdentity') }}</view>
<view v-if="language !== 'en'" class="role-tab-subtitle">{{ t('domesticIdentityHint') }}</view>
</view>
</view>
</view>
@@ -112,6 +114,36 @@
<input class="form-input" :placeholder="t('workUnitPlaceholder')" placeholder-class="input-placeholder" v-model="form.workUnit" />
</view>
<view class="form-item">
<view class="form-label">{{ t('admissionNoticeAttachmentLabel') }}</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">{{ t('uploadAdmissionNotice') }}</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">{{ t('imageUploadedTip') }}</view>
</view>
<view class="attachment-action-row">
<view class="attachment-action-btn" @click.stop="previewAttachment('admissionNoticeAttachment')">
{{ t('viewImage') }}
</view>
<view class="attachment-action-btn is-primary" @click.stop="uploadAttachment('admissionNoticeAttachment')">
{{ t('reupload') }}
</view>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">{{ t('hangzhouPropertyLabel') }} <text class="required-mark">*</text></view>
<view class="tn-flex tn-flex-wrap">
@@ -120,6 +152,20 @@
</view>
</view>
<view class="form-item">
<view class="form-label">{{ t('idCardTypeLabel') }} <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">{{ t('idCardTypeFixedText') }}</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 || t('idCardTypePlaceholder') }}</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">{{ t('idCardLabel') }} <text class="required-mark">*</text></view>
@@ -189,12 +235,12 @@
<template v-else>
<view class="form-item">
<view class="form-label">{{ t('passportLabel') }} <text class="required-mark">*</text></view>
<input class="form-input" :placeholder="t('passportPlaceholder')" placeholder-class="input-placeholder" v-model="form.passportNumber" />
<view 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">{{ t('passportExpiryLabel') }} <text class="required-mark">*</text></view>
<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 || t('selectDate') }}</text>
@@ -209,11 +255,11 @@
</view>
<view class="form-item" style="margin-bottom: 0;">
<view class="form-label">{{ t('passportAttachmentLabel') }} <text class="required-mark">*</text></view>
<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">{{ t('uploadPassport') }}</view>
<view class="tn-margin-top-sm upload-text">{{ foreignDocumentUploadText }}</view>
</view>
</view>
<view v-else class="attachment-file-card">
@@ -230,7 +276,7 @@
<view class="attachment-file-info">
<view class="attachment-name">{{ form.passportAttachment.name }}</view>
<view class="attachment-type tn-margin-top-xs">
{{ isImageAttachment(form.passportAttachment) ? t('imageUploadedTip') : t('passportImageOnlyTip') }}
{{ isImageAttachment(form.passportAttachment) ? t('imageUploadedTip') : foreignDocumentImageOnlyTip }}
</view>
</view>
<view class="attachment-action-row">
@@ -308,6 +354,30 @@
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<view v-if="showPrivacyPopup" class="privacy-mask">
<view class="privacy-dialog">
<view class="privacy-title">{{ t('privacyTitle') }}</view>
<view class="privacy-content">
{{ t('privacyContentPrefix') }}
<text class="privacy-link" @click="openPrivacyContract">{{ t('privacyContractName') }}</text>
{{ t('privacyContentSuffix') }}
</view>
<view class="privacy-action-row">
<view class="privacy-action-btn" @click="handlePrivacyDisagree">{{ t('privacyDisagree') }}</view>
<button
id="register-privacy-agree-btn"
class="privacy-action-btn is-primary"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
>
{{ t('privacyAgree') }}
</button>
</view>
</view>
</view>
<!-- #endif -->
</view>
</template>
@@ -320,7 +390,8 @@ import {
} from '../../utils/app-state'
import { getCountryList, submitRegistrationApplication, sysdictPage, uploadCommonFile, registrationMyorder } from '@/api/index.js'
import { addFilePrefix } from '@/utils/common.js'
import { getGenderLabel } from '../../utils/enum-store'
import { bindPrivacyAuthorizationListener, openWechatPrivacyContract, unbindPrivacyAuthorizationListener } from '@/utils/privacy.js'
import { fetchEnums, getEnumOptions, getGenderLabel } from '../../utils/enum-store'
import { resolveRegistrationApplicationCode } from '../../utils/flow'
function createAttachmentState() {
@@ -342,6 +413,7 @@ function createDefaultForm() {
gender: 1,
phone: '',
country: '',
idCardType: 2,
idCard: '',
workUnit: '',
hasHangzhouProperty: null,
@@ -351,6 +423,7 @@ function createDefaultForm() {
passportExpiry: '',
school: '',
religion: '',
admissionNoticeAttachment: createAttachmentState(),
passportAttachment: createAttachmentState(),
idCardFrontAttachment: createAttachmentState(),
idCardBackAttachment: createAttachmentState()
@@ -374,7 +447,9 @@ function normalizeRegistrationDraftForm(draft) {
...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)
@@ -391,21 +466,27 @@ export default {
draftPersistTimer: null,
draftTipText: '',
lastForeignCountry: '',
lastForeignIdCardType: 2,
showPrivacyPopup: false,
resolvePrivacyAuthorization: null,
form: createDefaultForm(),
rawCountryList: [],
rawHobbyList: [],
rawDietaryHabitList: [],
rawReligionList: [],
rawIdCardTypeList: [],
countryOptions: [],
hobbyOptions: [],
dietaryHabitOptions: [],
religionOptions: [],
idCardTypeOptions: [],
i18n: {
zh: {
navTitle: '注册账号',
pageTitle: '注册并提交实名资料',
pageDesc: '账号信息和实名认证资料需要一起填写完成,提交的是注册申请单,审批通过后才算注册成功。',
accountSection: '账号信息',
admissionNoticeAttachmentLabel: '入学通知书(在职证明)附件',
accountLabel: '登录账号',
accountPlaceholder: '请输入登录账号',
passwordLabel: '登录密码',
@@ -416,7 +497,9 @@ export default {
emailCodePlaceholder: '请输入邮箱验证码',
basicSection: '基础实名信息',
foreignIdentity: '外籍人员',
foreignIdentityHint: 'Foreign',
domesticIdentity: '中国籍',
domesticIdentityHint: 'Chinese',
nameLabel: '姓名',
namePlaceholder: '请输入真实姓名',
genderLabel: '性别',
@@ -434,6 +517,9 @@ export default {
hangzhouPropertyLabel: '杭州有无房产',
yesText: '有',
noText: '无',
idCardTypeLabel: '证件类型',
idCardTypePlaceholder: '请选择证件类型',
idCardTypeFixedText: '中国籍固定',
idCardLabel: '身份证号',
idCardPlaceholder: '请输入身份证号',
idCardFrontAttachmentLabel: '身份证正面附件',
@@ -446,12 +532,18 @@ export default {
passportLabel: '护照号',
passportPlaceholder: '请输入护照号',
passportExpiryLabel: '护照到期时间',
documentNumberLabel: '证件号',
documentNumberPlaceholder: '请输入证件号',
documentExpiryLabel: '证件到期时间',
selectDate: '请选择日期',
schoolLabel: '就读学校',
schoolPlaceholder: '请输入就读学校(选填)',
passportAttachmentLabel: '护照附件',
documentAttachmentLabel: '证件附件',
uploadPassport: '点击上传护照图片',
uploadDocument: '点击上传证件图片',
passportImageOnlyTip: '护照附件仅支持图片,请重新上传',
documentImageOnlyTip: '证件附件仅支持图片,请重新上传',
preferenceSection: '偏好信息',
dietaryLabel: '饮食习惯',
hobbyLabel: '兴趣爱好',
@@ -478,20 +570,32 @@ export default {
uploading: '上传中...',
uploadSuccess: '上传成功',
uploadFailed: '上传失败,请重试',
privacyTitle: '隐私授权提示',
privacyContentPrefix: '上传证件图片前,请先阅读并同意',
privacyContractName: '《用户隐私保护指引》',
privacyContentSuffix: ',同意后才能继续选择图片。',
privacyAgree: '同意并继续',
privacyDisagree: '暂不授权',
privacyAuthorizeFailed: '未完成隐私授权,暂时无法选择图片',
attachmentNotImage: '当前附件不是图片',
uploadImageOption: '上传图片',
submitPending: '申请已提交,待审批',
submitFailed: '提交失败,请稍后重试',
draftTipDefault: '未完成内容会自动保存,可随时返回继续填写',
draftTipSaved: '当前填写内容已自动保存',
uploadAdmissionNotice: '点击上传入学通知书/在职证明',
validateRequiredBasic: '请先填写账号、密码、邮箱和姓名',
validatePasswordLength: '登录密码至少 6 位',
validateEmail: '请输入正确的邮箱地址',
validateIdentityBasic: '请完善性别、手机号和国籍',
validatePhone: '请输入正确的手机号',
validateProfile: '请完善工作单位和杭州房产情况',
validateIdCardType: '请选择证件类型',
validateIdCard: '请填写身份证号',
validateIdCardAttachments: '请上传身份证正反面附件',
validateDocumentBasic: '请填写证件号和证件到期时间',
validateDocumentAttachment: '请上传证件图片',
validateDocumentAttachmentImage: '证件附件仅支持图片格式',
validatePassportBasic: '请填写护照号和护照到期时间',
validatePassportAttachment: '请上传护照图片',
validatePassportAttachmentImage: '护照附件仅支持图片格式'
@@ -501,6 +605,7 @@ export default {
pageTitle: 'Register and Submit Verification',
pageDesc: 'Account information and identity verification materials must be completed together. Registration succeeds only after approval.',
accountSection: 'Account Information',
admissionNoticeAttachmentLabel: 'Admission Notice (Employment Certificate)',
accountLabel: 'Account',
accountPlaceholder: 'Enter account name',
passwordLabel: 'Password',
@@ -511,7 +616,9 @@ export default {
emailCodePlaceholder: 'Enter email verification code',
basicSection: 'Basic Verification',
foreignIdentity: 'Foreign',
foreignIdentityHint: 'Foreign',
domesticIdentity: 'Chinese',
domesticIdentityHint: 'Chinese',
nameLabel: 'Name',
namePlaceholder: 'Enter your real name',
genderLabel: 'Gender',
@@ -529,6 +636,9 @@ export default {
hangzhouPropertyLabel: 'Property in Hangzhou',
yesText: 'Yes',
noText: 'No',
idCardTypeLabel: 'Document Type',
idCardTypePlaceholder: 'Select document type',
idCardTypeFixedText: 'Fixed for Chinese users',
idCardLabel: 'ID Card Number',
idCardPlaceholder: 'Enter ID card number',
idCardFrontAttachmentLabel: 'ID Card Front',
@@ -541,12 +651,18 @@ export default {
passportLabel: 'Passport Number',
passportPlaceholder: 'Enter passport number',
passportExpiryLabel: 'Passport Expiry Date',
documentNumberLabel: 'Document Number',
documentNumberPlaceholder: 'Enter document number',
documentExpiryLabel: 'Document Expiry Date',
selectDate: 'Select date',
schoolLabel: 'School',
schoolPlaceholder: 'Enter school (optional)',
passportAttachmentLabel: 'Passport Attachment',
documentAttachmentLabel: 'Document Attachment',
uploadPassport: 'Upload passport image',
uploadDocument: 'Upload document image',
passportImageOnlyTip: 'Passport attachment only supports images, please re-upload',
documentImageOnlyTip: 'Document attachment only supports images, please re-upload',
preferenceSection: 'Preferences',
dietaryLabel: 'Dietary Habit',
hobbyLabel: 'Hobby',
@@ -573,20 +689,32 @@ export default {
uploading: 'Uploading...',
uploadSuccess: 'Upload successful',
uploadFailed: 'Upload failed, please retry',
privacyTitle: 'Privacy Authorization',
privacyContentPrefix: 'Before uploading document images, please read and agree to the ',
privacyContractName: 'Privacy Protection Guide',
privacyContentSuffix: ' to continue selecting images.',
privacyAgree: 'Agree and Continue',
privacyDisagree: 'Not Now',
privacyAuthorizeFailed: 'Privacy authorization is required before selecting images',
attachmentNotImage: 'The current attachment is not an image',
uploadImageOption: 'Upload Image',
submitPending: 'Application submitted, pending approval',
submitFailed: 'Submission failed, please try again later',
draftTipDefault: 'Unfinished content is auto-saved and can be continued later',
draftTipSaved: 'Current content has been auto-saved',
uploadAdmissionNotice: 'Upload admission notice or employment certificate',
validateRequiredBasic: 'Please complete the account, password, email, and name fields first',
validatePasswordLength: 'Password must be at least 6 characters',
validateEmail: 'Please enter a valid email address',
validateIdentityBasic: 'Please complete gender, phone number, and country',
validatePhone: 'Please enter a valid phone number',
validateProfile: 'Please complete work unit and property status in Hangzhou',
validateIdCardType: 'Please select a document type',
validateIdCard: 'Please enter your ID card number',
validateIdCardAttachments: 'Please upload both sides of the ID card',
validateDocumentBasic: 'Please enter the document number and expiry date',
validateDocumentAttachment: 'Please upload the document image',
validateDocumentAttachmentImage: 'Document attachment only supports image files',
validatePassportBasic: 'Please enter passport number and expiry date',
validatePassportAttachment: 'Please upload the passport image',
validatePassportAttachmentImage: 'Passport attachment only supports image files'
@@ -607,6 +735,9 @@ export default {
countryRange() {
return this.countryOptions.length ? this.countryOptions.map(item => item.value) : [this.t('noSelectableOptions')]
},
idCardTypeRange() {
return this.idCardTypeOptions.length ? this.idCardTypeOptions.map(item => item.value) : [this.t('noSelectableOptions')]
},
dietaryHabitRange() {
return this.dietaryHabitOptions.length ? this.dietaryHabitOptions.map(item => item.value) : [this.t('noSelectableOptions')]
},
@@ -616,6 +747,31 @@ export default {
religionRange() {
return this.religionOptions.length ? this.religionOptions.map(item => item.value) : [this.t('noSelectableOptions')]
},
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.t('passportLabel') : this.t('documentNumberLabel')
},
foreignDocumentNumberPlaceholder() {
return this.isPassportType ? this.t('passportPlaceholder') : this.t('documentNumberPlaceholder')
},
foreignDocumentExpiryLabel() {
return this.isPassportType ? this.t('passportExpiryLabel') : this.t('documentExpiryLabel')
},
foreignDocumentAttachmentLabel() {
return this.isPassportType ? this.t('passportAttachmentLabel') : this.t('documentAttachmentLabel')
},
foreignDocumentUploadText() {
return this.isPassportType ? this.t('uploadPassport') : this.t('uploadDocument')
},
foreignDocumentImageOnlyTip() {
return this.isPassportType ? this.t('passportImageOnlyTip') : this.t('documentImageOnlyTip')
},
genderOptions() {
return [
{ key: 1, value: this.t('genderMale') },
@@ -637,6 +793,7 @@ export default {
},
async onLoad(options) {
this.draftTipText = this.t('draftTipDefault')
this.initPrivacyAuthorization()
const bypass = options && options.bypass === '1'
const hasRedirected = await this.checkPendingRegistration(bypass)
if (hasRedirected) return
@@ -645,7 +802,6 @@ export default {
this.loadDictOptions(),
this.loadCountryOptions()
])
this.tryRestoreFailedDraft()
},
onHide() {
@@ -654,6 +810,8 @@ export default {
onUnload() {
this.persistRegistrationDraft()
this.clearDraftPersistTimer()
this.resolvePrivacyAuthorization = null
this.releasePrivacyAuthorization()
},
onPageScroll(e) {
let opacity = e.scrollTop / 80
@@ -733,6 +891,10 @@ export default {
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.t('restoreDraftSuccess'),
@@ -760,6 +922,7 @@ export default {
},
hasDraftContent() {
const attachmentFields = [
'admissionNoticeAttachment',
'passportAttachment',
'idCardFrontAttachment',
'idCardBackAttachment'
@@ -820,6 +983,7 @@ export default {
this.lastForeignCountry = this.form.country
}
this.form.identityType = type
this.applyIdentityDocumentTypeRule()
this.applyIdentityCountryRule()
},
onPassportDateChange(e) {
@@ -846,15 +1010,19 @@ export default {
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())
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)
@@ -933,13 +1101,27 @@ export default {
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
},
@@ -970,6 +1152,21 @@ export default {
const selected = options[event.detail.value]
this.form[field] = selected ? selected.value : ''
},
onIdCardTypeChange(event) {
if (!this.idCardTypeOptions.length) {
uni.showToast({
title: this.t('noSelectableOptions'),
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) {
@@ -983,8 +1180,106 @@ export default {
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.chooseImageFile()
return this.ensurePrivacyAuthorized()
.then(() => this.chooseImageFile())
.then((file) => {
if (!file || !file.path) return
uni.showLoading({
@@ -1008,6 +1303,13 @@ export default {
})
.catch((error) => {
if (error && error.cancel) return
if (error && error.privacyDenied) {
uni.showToast({
title: this.t('privacyAuthorizeFailed'),
icon: 'none'
})
return
}
console.error('upload attachment failed', error)
uni.showToast({
title: this.t('uploadFailed'),
@@ -1103,8 +1405,6 @@ export default {
uni.showLoading({
title: this.t('submitting')
})
const authProfile = this.buildAuthProfile()
return this.getWechatCode().then((providerValue) => {
const submitPayload = this.buildSubmitPayload(providerValue)
@@ -1179,6 +1479,7 @@ export default {
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 = '') {
@@ -1204,6 +1505,9 @@ export default {
if (!this.form.workUnit || this.form.hasHangzhouProperty === null) {
return this.t('validateProfile')
}
if (!this.form.idCardType) {
return this.t('validateIdCardType')
}
if (this.form.identityType === 'DomesticStudents') {
if (!this.form.idCard) {
@@ -1216,13 +1520,13 @@ export default {
}
if (!this.form.passportNumber || !this.form.passportExpiry) {
return this.t('validatePassportBasic')
return this.isPassportType ? this.t('validatePassportBasic') : this.t('validateDocumentBasic')
}
if (!this.form.passportAttachment.serverValue) {
return this.t('validatePassportAttachment')
return this.isPassportType ? this.t('validatePassportAttachment') : this.t('validateDocumentAttachment')
}
if (!this.isImageAttachment(this.form.passportAttachment)) {
return this.t('validatePassportAttachmentImage')
return this.isPassportType ? this.t('validatePassportAttachmentImage') : this.t('validateDocumentAttachmentImage')
}
return ''
},
@@ -1246,6 +1550,8 @@ export default {
genderText: getGenderLabel(this.form.gender, this.form.gender === 1 ? this.t('genderMale') : this.form.gender === 0 ? this.t('genderFemale') : ''),
phone: this.form.phone,
country: this.form.country,
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,
@@ -1260,6 +1566,7 @@ export default {
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 : ''
@@ -1278,6 +1585,8 @@ export default {
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,
@@ -1287,6 +1596,7 @@ export default {
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
@@ -1469,11 +1779,21 @@ export default {
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 {
@@ -1736,4 +2056,84 @@ export default {
.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>