chore: initial import
This commit is contained in:
629
pkgAccount/auth_result/auth_result.vue
Normal file
629
pkgAccount/auth_result/auth_result.vue
Normal file
@@ -0,0 +1,629 @@
|
||||
<template>
|
||||
<view class="auth-result-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
|
||||
</tn-nav-bar>
|
||||
<view class="page-content tn-padding" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
|
||||
<view class="status-card fade-in">
|
||||
<!-- 状态大图标 -->
|
||||
<view class="status-icon-wrapper tn-flex tn-flex-row-center tn-flex-col-center" :class="authStatus">
|
||||
<text v-if="authStatus === 'approved'" class="tn-icon-success-circle-fill status-icon icon-success"></text>
|
||||
<text v-else-if="authStatus === 'rejected'" class="tn-icon-close-circle-fill status-icon icon-rejected"></text>
|
||||
<text v-else-if="authStatus === 'pending'" class="tn-icon-time-fill status-icon icon-pending"></text>
|
||||
<text v-else class="tn-icon-warning-fill status-icon icon-unsubmitted"></text>
|
||||
</view>
|
||||
|
||||
<!-- 状态标题与描述 -->
|
||||
<view class="tn-text-center tn-margin-top-lg tn-padding-left-lg tn-padding-right-lg">
|
||||
<view class="card-title">{{ titleText }}</view>
|
||||
<!-- <view class="card-desc tn-margin-top-sm">{{ descText }}</view> -->
|
||||
</view>
|
||||
|
||||
<!-- <view v-if="applicationCode" class="code-chip tn-margin-top-lg">
|
||||
申请编号:{{ applicationCode }}
|
||||
</view> -->
|
||||
|
||||
<!-- 审批备注 -->
|
||||
<view v-if="approvalRemark" class="reason-box tn-margin-top-xl tn-margin-left-lg tn-margin-right-lg" :class="authStatus">
|
||||
<view class="reason-title tn-flex tn-flex-row-center tn-flex-col-center">
|
||||
<text :class="remarkIconClass" class="tn-margin-right-xs"></text>{{ approvalRemarkTitle }}
|
||||
</view>
|
||||
<view class="reason-content tn-margin-top-sm tn-text-center">{{ approvalRemark }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交信息摘要 -->
|
||||
<view v-if="profileSummary.length" class="summary-box tn-margin-top-xl tn-margin-bottom-lg tn-margin-left-lg tn-margin-right-lg">
|
||||
<view class="summary-title tn-margin-bottom-sm">{{ t('submittedInfoTitle') }}</view>
|
||||
<view v-for="item in profileSummary" :key="item.label" class="summary-row tn-flex tn-flex-row-between tn-padding-top-sm tn-padding-bottom-sm">
|
||||
<text class="summary-label">{{ item.label }}</text>
|
||||
<text class="summary-value tn-text-ellipsis">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-group tn-margin-top-xl fade-in-up">
|
||||
<view v-if="authStatus === 'approved'" class="custom-btn btn-primary" @click="goToLogin">
|
||||
<text class="tn-text-bold">{{ t('goLogin') }}</text>
|
||||
</view>
|
||||
<view v-else-if="authStatus === 'rejected'" class="custom-btn btn-primary" @click="resubmit">
|
||||
<text class="tn-text-bold">{{ t('resubmitRegister') }}</text>
|
||||
</view>
|
||||
<view v-else class="custom-btn btn-dark" @click="refreshStatus">
|
||||
<text class="tn-text-bold tn-flex tn-flex-row-center tn-flex-col-center">
|
||||
<text v-if="loading" class="tn-icon-loading tn-margin-right-xs spin"></text>
|
||||
{{ loading ? t('refreshing') : t('refreshStatus') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="custom-btn btn-light tn-margin-top" @click="goToRegistrationRecords">
|
||||
<text class="tn-text-bold">{{ t('viewRegistrationRecords') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { saveRegisteredUser } from '../../utils/app-state'
|
||||
import { registrationApplicationMePage, registrationMyorder } from '@/api/index.js'
|
||||
import { fetchEnums, getApprovalStatusLabel } from '../../utils/enum-store'
|
||||
import { buildUserFromRegistrationApplication, mapRegistrationApprovalToAuthStatus, resolveRegistrationApplicationCode } from '../../utils/flow'
|
||||
|
||||
function formatPageResponse(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
return payload && typeof payload === 'object' ? payload : {}
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
applicationInfo: null,
|
||||
targetCode: '',
|
||||
sourceType: '',
|
||||
i18n: {
|
||||
zh: {
|
||||
navTitle: '注册申请结果',
|
||||
submittedInfoTitle: '已提交信息',
|
||||
goLogin: '去登录',
|
||||
resubmitRegister: '重新提交注册申请',
|
||||
refreshing: '刷新中...',
|
||||
refreshStatus: '刷新审批状态',
|
||||
viewRegistrationRecords: '查看注册记录',
|
||||
unsubmitted: '未提交',
|
||||
titleUnsubmitted: '您还未提交注册申请',
|
||||
titlePending: '注册申请已提交',
|
||||
titleApproved: '注册审批已通过',
|
||||
titleRejected: '注册审批未通过',
|
||||
descUnsubmitted: '请先提交注册申请单,审批通过后才算注册成功。',
|
||||
descPendingRecords: '当前展示的是该申请单的审批状态,可下拉或点击按钮刷新最新结果。',
|
||||
descPendingDefault: '当前申请正在审批中,审批通过前无法登录。',
|
||||
descApproved: '您的注册申请已审批通过,现在可以前往登录。',
|
||||
descRejectedRecords: '当前展示的是该申请单的审批结果,请根据驳回原因修改信息后重新提交注册申请。',
|
||||
descRejectedDefault: '请根据驳回原因修改信息后重新提交注册申请。',
|
||||
remarkApproved: '通过备注',
|
||||
remarkRejected: '驳回原因',
|
||||
remarkPending: '审批备注',
|
||||
roleForeign: '外籍人员',
|
||||
roleDomestic: '中国籍',
|
||||
labelIdentityType: '人员类型',
|
||||
labelName: '姓名',
|
||||
labelAccount: '注册账号',
|
||||
labelCertificateNo: '证件号',
|
||||
fallbackNone: '-',
|
||||
latestDetailMissing: '未查到最新详情,已展示列表记录',
|
||||
approvedPleaseLogin: '审批已通过,请登录',
|
||||
fetchStatusFailed: '获取审批状态失败',
|
||||
statusPending: '审核中',
|
||||
statusApproved: '已通过',
|
||||
statusRejected: '已驳回'
|
||||
},
|
||||
en: {
|
||||
navTitle: 'Registration Result',
|
||||
submittedInfoTitle: 'Submitted Information',
|
||||
goLogin: 'Sign In',
|
||||
resubmitRegister: 'Submit Registration Again',
|
||||
refreshing: 'Refreshing...',
|
||||
refreshStatus: 'Refresh Status',
|
||||
viewRegistrationRecords: 'View Registration Records',
|
||||
unsubmitted: 'Not Submitted',
|
||||
titleUnsubmitted: 'No registration application submitted yet',
|
||||
titlePending: 'Registration submitted',
|
||||
titleApproved: 'Registration approved',
|
||||
titleRejected: 'Registration was not approved',
|
||||
descUnsubmitted: 'Please submit a registration application first. Registration is completed only after approval.',
|
||||
descPendingRecords: 'This page shows the approval status of the selected application. Pull down or tap the button to refresh.',
|
||||
descPendingDefault: 'Your application is under review and sign-in is unavailable before approval.',
|
||||
descApproved: 'Your registration is approved and you can sign in now.',
|
||||
descRejectedRecords: 'This page shows the approval result of the selected application. Please update the information and resubmit based on the rejection reason.',
|
||||
descRejectedDefault: 'Please update the information and resubmit based on the rejection reason.',
|
||||
remarkApproved: 'Approval Note',
|
||||
remarkRejected: 'Rejection Reason',
|
||||
remarkPending: 'Approval Note',
|
||||
roleForeign: 'Foreign',
|
||||
roleDomestic: 'Chinese',
|
||||
labelIdentityType: 'Identity Type',
|
||||
labelName: 'Name',
|
||||
labelAccount: 'Account',
|
||||
labelCertificateNo: 'Certificate No.',
|
||||
fallbackNone: '-',
|
||||
latestDetailMissing: 'Latest details are unavailable. Showing the list record instead.',
|
||||
approvedPleaseLogin: 'Approved. Please sign in.',
|
||||
fetchStatusFailed: 'Failed to fetch approval status',
|
||||
statusPending: 'Under Review',
|
||||
statusApproved: 'Approved',
|
||||
statusRejected: 'Rejected'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
},
|
||||
authStatus() {
|
||||
const status = this.applicationInfo ? this.applicationInfo.approvalStatus : undefined
|
||||
return mapRegistrationApprovalToAuthStatus(status)
|
||||
},
|
||||
applicationCode() {
|
||||
return this.targetCode || resolveRegistrationApplicationCode(this.applicationInfo) || ''
|
||||
},
|
||||
approvalStatusText() {
|
||||
const status = this.applicationInfo ? this.applicationInfo.approvalStatus : undefined
|
||||
if (typeof status === 'number') {
|
||||
return this.getApprovalStatusLabelLocal(status)
|
||||
}
|
||||
return this.t('unsubmitted')
|
||||
},
|
||||
titleText() {
|
||||
const map = {
|
||||
unsubmitted: this.t('titleUnsubmitted'),
|
||||
pending: this.t('titlePending'),
|
||||
approved: this.t('titleApproved'),
|
||||
rejected: this.t('titleRejected')
|
||||
}
|
||||
return map[this.authStatus] || map.unsubmitted
|
||||
},
|
||||
descText() {
|
||||
const map = {
|
||||
unsubmitted: this.t('descUnsubmitted'),
|
||||
pending: this.sourceType === 'registration-records'
|
||||
? this.t('descPendingRecords')
|
||||
: this.t('descPendingDefault'),
|
||||
approved: this.t('descApproved'),
|
||||
rejected: this.sourceType === 'registration-records'
|
||||
? this.t('descRejectedRecords')
|
||||
: this.t('descRejectedDefault')
|
||||
}
|
||||
return map[this.authStatus] || map.unsubmitted
|
||||
},
|
||||
approvalRemarkTitle() {
|
||||
const map = {
|
||||
approved: this.t('remarkApproved'),
|
||||
rejected: this.t('remarkRejected'),
|
||||
pending: this.t('remarkPending'),
|
||||
unsubmitted: this.t('remarkPending')
|
||||
}
|
||||
return map[this.authStatus] || map.pending
|
||||
},
|
||||
approvalRemark() {
|
||||
return this.applicationInfo && this.applicationInfo.approveReason
|
||||
? this.applicationInfo.approveReason
|
||||
: ''
|
||||
},
|
||||
remarkIconClass() {
|
||||
const map = {
|
||||
approved: 'tn-icon-success',
|
||||
rejected: 'tn-icon-warning',
|
||||
pending: 'tn-icon-time',
|
||||
unsubmitted: 'tn-icon-warning'
|
||||
}
|
||||
return map[this.authStatus] || map.pending
|
||||
},
|
||||
profileSummary() {
|
||||
const profile = this.applicationInfo || {}
|
||||
const identityType = profile.passportNumber ? 'foreign' : 'domestic'
|
||||
const roleText = identityType === 'foreign' ? this.t('roleForeign') : identityType === 'domestic' ? this.t('roleDomestic') : this.t('fallbackNone')
|
||||
const certificateNo = profile.passportNumber || profile.idCard || this.t('fallbackNone')
|
||||
return [
|
||||
{ label: this.t('labelIdentityType'), value: roleText },
|
||||
{ label: this.t('labelName'), value: profile.name || this.t('fallbackNone') },
|
||||
{ label: this.t('labelAccount'), value: profile.account || this.t('fallbackNone') },
|
||||
{ label: this.t('labelCertificateNo'), value: certificateNo },
|
||||
// { label: '国籍', value: profile.country || profile.nationality || '-' },
|
||||
// { label: '工作单位', value: profile.workUnit || '-' },
|
||||
// { label: '兴趣爱好', value: hobbyText }
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
language() {
|
||||
this.refreshStatus()
|
||||
}
|
||||
},
|
||||
onLoad(options = {}) {
|
||||
this.sourceType = options.from || ''
|
||||
this.targetCode = decodeURIComponent(options.code || '').trim()
|
||||
|
||||
const payloadText = options.payload ? decodeURIComponent(options.payload) : ''
|
||||
if (!payloadText) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(payloadText)
|
||||
if (payload && typeof payload === 'object') {
|
||||
this.applicationInfo = payload
|
||||
if (!this.targetCode) {
|
||||
this.targetCode = resolveRegistrationApplicationCode(payload)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('parse auth result payload failed', error)
|
||||
}
|
||||
},
|
||||
async onShow() {
|
||||
await fetchEnums(false, this.language)
|
||||
this.refreshStatus()
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgAccount.auth_result.auth_result', key)
|
||||
},
|
||||
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
|
||||
})
|
||||
},
|
||||
async fetchApplicationByCode(providerValue, applicationCode) {
|
||||
if (!providerValue || !applicationCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
const response = await registrationApplicationMePage({
|
||||
ProviderValue: providerValue,
|
||||
Search: applicationCode,
|
||||
PageIndex: 1,
|
||||
PageSize: 20
|
||||
}, this.language)
|
||||
const pageData = formatPageResponse(response)
|
||||
const list = Array.isArray(pageData.data) ? pageData.data : []
|
||||
return list.find(item => resolveRegistrationApplicationCode(item) === applicationCode) || list[0] || null
|
||||
},
|
||||
async refreshStatus() {
|
||||
this.loading = true
|
||||
try {
|
||||
const providerValue = await this.getWechatCode()
|
||||
const applicationCode = this.applicationCode
|
||||
let target = null
|
||||
|
||||
if (applicationCode) {
|
||||
target = await this.fetchApplicationByCode(providerValue || '', applicationCode)
|
||||
} else {
|
||||
const response = await registrationMyorder({
|
||||
providerValue: providerValue || ''
|
||||
}, this.language)
|
||||
const rawTarget = response && response.data && typeof response.data === 'object' ? response.data : response
|
||||
target = rawTarget.data !== undefined ? rawTarget.data : rawTarget
|
||||
}
|
||||
|
||||
if (!target || typeof target !== 'object' || !Object.keys(target).length) {
|
||||
if (this.applicationInfo && this.applicationCode) {
|
||||
uni.showToast({
|
||||
title: this.t('latestDetailMissing'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.applicationInfo = {
|
||||
approvalStatus: 1
|
||||
}
|
||||
uni.showToast({
|
||||
title: this.t('approvedPleaseLogin'),
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.goToLogin()
|
||||
}, 800)
|
||||
return
|
||||
}
|
||||
|
||||
this.applicationInfo = target
|
||||
this.targetCode = resolveRegistrationApplicationCode(target) || this.targetCode
|
||||
if (target.approvalStatus === 1) {
|
||||
const approvedUser = buildUserFromRegistrationApplication(target, {
|
||||
password: ''
|
||||
})
|
||||
saveRegisteredUser(approvedUser)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('refresh registration status failed', error)
|
||||
uni.showToast({
|
||||
title: this.t('fetchStatusFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getApprovalStatusLabelLocal(status) {
|
||||
return getApprovalStatusLabel(
|
||||
status,
|
||||
this.authStatus === 'pending'
|
||||
? this.t('statusPending')
|
||||
: this.authStatus === 'approved'
|
||||
? this.t('statusApproved')
|
||||
: this.authStatus === 'rejected'
|
||||
? this.t('statusRejected')
|
||||
: this.t('unsubmitted'),
|
||||
this.language
|
||||
)
|
||||
},
|
||||
goToLogin() {
|
||||
uni.reLaunch({
|
||||
url: '/pkgAccount/login/login?bypass=1'
|
||||
})
|
||||
},
|
||||
resubmit() {
|
||||
uni.redirectTo({
|
||||
url: '/pkgAccount/register/register?bypass=1'
|
||||
})
|
||||
},
|
||||
goToRegistrationRecords() {
|
||||
uni.navigateTo({
|
||||
url: '/pkgAccount/registration_records/registration_records'
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.auth-result-page {
|
||||
/* 白蓝主题渐变背景 */
|
||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
border: 1rpx solid rgba($brand-primary, 0.05);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 120rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.status-icon-wrapper {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
margin-top: -70rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx rgba($brand-primary, 0.08);
|
||||
}
|
||||
|
||||
.status-icon-wrapper.approved {
|
||||
background: rgba($brand-primary, 0.1);
|
||||
}
|
||||
|
||||
.status-icon-wrapper.rejected {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.status-icon-wrapper.pending,
|
||||
.status-icon-wrapper.unsubmitted {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.icon-rejected {
|
||||
color: #cf3c3c;
|
||||
}
|
||||
|
||||
.icon-pending,
|
||||
.icon-unsubmitted {
|
||||
color: #c98512;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 44rpx;
|
||||
font-weight: bold;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 28rpx;
|
||||
color: #888888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.code-chip {
|
||||
width: fit-content;
|
||||
max-width: calc(100% - 64rpx);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba($brand-primary, 0.08);
|
||||
color: $brand-primary;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.reason-box {
|
||||
padding: 30rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.reason-box.approved {
|
||||
background: rgba($brand-primary, 0.05);
|
||||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||||
}
|
||||
.reason-box.approved .reason-title,
|
||||
.reason-box.approved .reason-content {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.reason-box.rejected {
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
border: 1rpx solid rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
.reason-box.rejected .reason-title,
|
||||
.reason-box.rejected .reason-content {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.reason-box.pending,
|
||||
.reason-box.unsubmitted {
|
||||
background: rgba(245, 158, 11, 0.05);
|
||||
border: 1rpx solid rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
.reason-box.pending .reason-title,
|
||||
.reason-box.pending .reason-content,
|
||||
.reason-box.unsubmitted .reason-title,
|
||||
.reason-box.unsubmitted .reason-content {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.reason-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reason-content {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.summary-box {
|
||||
padding: 30rpx;
|
||||
background: rgba($brand-primary, 0.02);
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
border-bottom: 1rpx dashed rgba($brand-primary, 0.15);
|
||||
}
|
||||
.summary-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 28rpx;
|
||||
color: #6A7A99;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 28rpx;
|
||||
color: #1A2233;
|
||||
font-weight: 500;
|
||||
max-width: 65%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.custom-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 32rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.custom-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
background: #ffffff;
|
||||
color: $brand-primary;
|
||||
border: 1rpx solid rgba($brand-primary, 0.3);
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||||
}
|
||||
|
||||
.btn-light {
|
||||
background: #ffffff;
|
||||
color: #1A2233;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.04);
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
animation-delay: 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(40rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
display: inline-block;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
577
pkgAccount/login/login.vue
Normal file
577
pkgAccount/login/login.vue
Normal file
@@ -0,0 +1,577 @@
|
||||
<template>
|
||||
<view class="login-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
|
||||
<!-- 可保留标题或留空保持简洁 -->
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="login-content tn-padding-xl" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
|
||||
|
||||
<!-- 页面头部大标题 -->
|
||||
<view class="page-header tn-margin-bottom-xl tn-margin-top-xl fade-in">
|
||||
<view class="tn-text-bold tn-color-black title-text">{{ t('pageTitle') }}</view>
|
||||
<view class="tn-color-gray tn-text-sm tn-margin-top-sm subtitle-text">{{ t('pageDesc') }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<view class="form-area tn-margin-top-xl fade-in-up">
|
||||
<view class="form-item tn-margin-bottom-lg">
|
||||
<view class="form-label tn-text-bold tn-margin-bottom-sm">{{ t('accountLabel') }}</view>
|
||||
<view class="input-wrapper" :class="{ 'is-focused': accountFocused }">
|
||||
<text class="tn-icon-my input-icon"></text>
|
||||
<input
|
||||
class="form-input"
|
||||
:placeholder="t('accountPlaceholder')"
|
||||
placeholder-class="input-placeholder"
|
||||
v-model="form.account"
|
||||
@focus="accountFocused = true"
|
||||
@blur="accountFocused = false"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-bottom-xl">
|
||||
<view class="form-label tn-text-bold tn-margin-bottom-sm">{{ t('passwordLabel') }}</view>
|
||||
<view class="input-wrapper" :class="{ 'is-focused': passwordFocused }">
|
||||
<text class="tn-icon-lock input-icon"></text>
|
||||
<input
|
||||
class="form-input"
|
||||
type="password"
|
||||
:placeholder="t('passwordPlaceholder')"
|
||||
placeholder-class="input-placeholder"
|
||||
v-model="form.password"
|
||||
@focus="passwordFocused = true"
|
||||
@blur="passwordFocused = false"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-area tn-margin-top-xxl fade-in-up" style="animation-delay: 0.2s;">
|
||||
<view class="custom-btn btn-primary" @click="handleLogin" :class="{ 'btn-disabled': isSubmitting }">
|
||||
<text v-if="isSubmitting" class="tn-icon-loading tn-margin-right-xs spin"></text>
|
||||
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? t('submitting') : t('submitLogin') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="secondary-entry-card tn-margin-top-lg" @click="goToRegistrationRecords">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view>
|
||||
<view class="secondary-entry-title">{{ t('viewRegistrationRecords') }}</view>
|
||||
<view class="secondary-entry-desc tn-margin-top-xs">{{ t('viewRegistrationRecordsDesc') }}</view>
|
||||
</view>
|
||||
<text class="tn-icon-right secondary-entry-arrow"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tn-flex tn-flex-row-center tn-margin-top-xl">
|
||||
<view class="tn-color-gray tn-text-sm">{{ t('noAccount') }}</view>
|
||||
<view class="tn-color-black tn-text-bold tn-text-sm tn-margin-left-xs register-link" @click="goToRegister">{{ t('goRegister') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRegisteredUser, saveRegisteredUser, startAuthorizedLoginSession } from '../../utils/app-state'
|
||||
import { mergeBackendUserInfo, normalizeUser, resolveRegistrationApplicationCode } from '../../utils/flow'
|
||||
import {
|
||||
extractLoginSession,
|
||||
matchRegisteredUser,
|
||||
pickLatestRegistration,
|
||||
resolveLoginPreparation
|
||||
} from '../../utils/login-helper'
|
||||
import { registrationMyorder, userMe, usersLogin } from '@/api/index.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isSubmitting: false,
|
||||
redirectUrl: '',
|
||||
accountFocused: false,
|
||||
passwordFocused: false,
|
||||
form: {
|
||||
account: '',
|
||||
password: ''
|
||||
},
|
||||
i18n: {
|
||||
zh: {
|
||||
pageTitle: '欢迎回来',
|
||||
pageDesc: '登录绿居公寓,探索您的国际化青年社区',
|
||||
accountLabel: '账号 / 邮箱',
|
||||
accountPlaceholder: '请输入账号或邮箱',
|
||||
passwordLabel: '密码',
|
||||
passwordPlaceholder: '请输入密码',
|
||||
submitting: '登录中...',
|
||||
submitLogin: '立即登录',
|
||||
viewRegistrationRecords: '查看注册记录',
|
||||
viewRegistrationRecordsDesc: '未登录也可查看当前微信账号提交的注册申请记录',
|
||||
noAccount: '还没有账号?',
|
||||
goRegister: '去注册',
|
||||
loading: '加载中...',
|
||||
validateRequired: '请输入账号和密码',
|
||||
loginSuccess: '登录成功',
|
||||
loginFailed: '登录失败,请稍后重试'
|
||||
},
|
||||
en: {
|
||||
pageTitle: 'Welcome Back',
|
||||
pageDesc: 'Sign in to Luju Apartments and explore your international youth community.',
|
||||
accountLabel: 'Account / Email',
|
||||
accountPlaceholder: 'Enter account or email',
|
||||
passwordLabel: 'Password',
|
||||
passwordPlaceholder: 'Enter password',
|
||||
submitting: 'Signing In...',
|
||||
submitLogin: 'Sign In',
|
||||
viewRegistrationRecords: 'View Registration Records',
|
||||
viewRegistrationRecordsDesc: 'You can review registration records submitted by this Wechat account even without signing in.',
|
||||
noAccount: 'No account yet?',
|
||||
goRegister: 'Register',
|
||||
loading: 'Loading...',
|
||||
validateRequired: 'Please enter both account and password',
|
||||
loginSuccess: 'Signed in successfully',
|
||||
loginFailed: 'Sign-in failed, please try again later'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async onLoad(options) {
|
||||
this.redirectUrl = this.normalizeRedirectUrl(options && options.redirect)
|
||||
const bypass = options && options.bypass === '1'
|
||||
await this.checkPendingRegistration(bypass)
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgAccount.login.login', key)
|
||||
},
|
||||
normalizeRedirectUrl(value = '') {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
if (String(value).startsWith('/')) {
|
||||
return String(value)
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch (error) {
|
||||
console.error('decode login redirect failed', error)
|
||||
return ''
|
||||
}
|
||||
},
|
||||
isHomePageUrl(url = '') {
|
||||
return /^\/pages\/index\/index(?:\?|$)/.test(String(url || ''))
|
||||
},
|
||||
navigateAfterLogin() {
|
||||
const targetUrl = this.redirectUrl
|
||||
const pages = getCurrentPages()
|
||||
if (targetUrl) {
|
||||
if (this.isHomePageUrl(targetUrl)) {
|
||||
uni.reLaunch({
|
||||
url: targetUrl
|
||||
})
|
||||
return
|
||||
}
|
||||
if (Array.isArray(pages) && pages.length > 1) {
|
||||
uni.redirectTo({
|
||||
url: targetUrl
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.reLaunch({
|
||||
url: targetUrl
|
||||
})
|
||||
return
|
||||
}
|
||||
if (Array.isArray(pages) && pages.length > 1) {
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
},
|
||||
async checkPendingRegistration(bypassRedirect = false) {
|
||||
uni.showLoading({ title: this.t('loading') })
|
||||
try {
|
||||
const providerValue = await this.getWechatCode()
|
||||
if (!providerValue) {
|
||||
return false
|
||||
}
|
||||
const response = await registrationMyorder({
|
||||
providerValue
|
||||
}, this.$getLanguage())
|
||||
|
||||
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
|
||||
} else {
|
||||
if (latest.approvalStatus === 1) {
|
||||
this.form.account = latest.account || latest.email || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('check pending registration failed', e)
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
return false
|
||||
},
|
||||
validateForm() {
|
||||
if (this.form.account && this.form.password) {
|
||||
return true
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: this.t('validateRequired'),
|
||||
icon: 'none'
|
||||
})
|
||||
return 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
|
||||
})
|
||||
},
|
||||
async fetchLatestRegistration() {
|
||||
const providerValue = await this.getWechatCode()
|
||||
const response = await registrationMyorder({
|
||||
Account: this.form.account,
|
||||
providerValue: providerValue || ''
|
||||
}, this.$getLanguage())
|
||||
|
||||
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) {
|
||||
return latest
|
||||
}
|
||||
return null
|
||||
},
|
||||
async loginByApi() {
|
||||
const response = await usersLogin({
|
||||
account: this.form.account,
|
||||
password: this.form.password
|
||||
}, this.$getLanguage())
|
||||
|
||||
return extractLoginSession(response, this.$getLanguage())
|
||||
},
|
||||
fetchUserInfoAfterLogin() {
|
||||
return userMe({}, this.$getLanguage())
|
||||
.then(userInfoRes => {
|
||||
return userInfoRes && userInfoRes.data ? userInfoRes.data : userInfoRes
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('fetch userinfo failed', error)
|
||||
return null
|
||||
})
|
||||
},
|
||||
rejectHandledLogin(options = {}) {
|
||||
const {
|
||||
title = '',
|
||||
navigateToAuthResult = false
|
||||
} = options
|
||||
|
||||
uni.hideLoading()
|
||||
this.isSubmitting = false
|
||||
|
||||
if (title) {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
if (navigateToAuthResult) {
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pkgAccount/auth_result/auth_result'
|
||||
})
|
||||
}, 600)
|
||||
}
|
||||
|
||||
throw { __handled: true }
|
||||
},
|
||||
async resolveLoginState() {
|
||||
const localRegisteredUser = normalizeUser(getRegisteredUser())
|
||||
const matchedLocalUser = matchRegisteredUser(localRegisteredUser, this.form.account)
|
||||
const { token, expires } = await this.loginByApi()
|
||||
const latestRegistration = await this.fetchLatestRegistration()
|
||||
const loginPreparation = resolveLoginPreparation({
|
||||
language: this.$getLanguage(),
|
||||
matchedLocalUser,
|
||||
latestRegistration,
|
||||
account: this.form.account,
|
||||
password: this.form.password
|
||||
})
|
||||
|
||||
if (!loginPreparation.ok) {
|
||||
this.rejectHandledLogin(loginPreparation.handledError)
|
||||
}
|
||||
return {
|
||||
sessionUser: loginPreparation.sessionUser,
|
||||
token,
|
||||
expires
|
||||
}
|
||||
},
|
||||
async finishLogin(loginState) {
|
||||
const {
|
||||
sessionUser,
|
||||
token,
|
||||
expires
|
||||
} = loginState
|
||||
|
||||
startAuthorizedLoginSession(sessionUser, token, expires)
|
||||
const backendUserInfo = await this.fetchUserInfoAfterLogin()
|
||||
const finalUser = mergeBackendUserInfo(sessionUser, backendUserInfo)
|
||||
|
||||
saveRegisteredUser(finalUser)
|
||||
startAuthorizedLoginSession(finalUser, token, expires)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: this.t('loginSuccess'),
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.isSubmitting = false
|
||||
this.navigateAfterLogin()
|
||||
}, 600)
|
||||
},
|
||||
handleLogin() {
|
||||
if (this.isSubmitting || !this.validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isSubmitting = true
|
||||
uni.showLoading({ title: this.t('submitting') })
|
||||
|
||||
return this.resolveLoginState()
|
||||
.then(loginState => {
|
||||
return this.finishLogin(loginState)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && error.__handled) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error('handle login failed', error)
|
||||
uni.hideLoading()
|
||||
this.isSubmitting = false
|
||||
uni.showToast({
|
||||
title: this.t('loginFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
goToRegister() {
|
||||
uni.navigateTo({
|
||||
url: '/pkgAccount/register/register'
|
||||
})
|
||||
},
|
||||
goToRegistrationRecords() {
|
||||
uni.navigateTo({
|
||||
url: '/pkgAccount/registration_records/registration_records'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
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%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 64rpx;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.form-label {
|
||||
font-size: 30rpx;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
padding: 0 30rpx;
|
||||
height: 108rpx;
|
||||
}
|
||||
|
||||
.input-wrapper.is-focused {
|
||||
border-color: $brand-primary;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.15);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
font-size: 36rpx;
|
||||
color: #cccccc;
|
||||
margin-right: 20rpx;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.input-wrapper.is-focused .input-icon {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: #bbbbbb;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 按钮与操作区 */
|
||||
.custom-btn {
|
||||
width: 100%;
|
||||
height: 108rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 999rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.7;
|
||||
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;
|
||||
}
|
||||
|
||||
.secondary-entry-desc {
|
||||
font-size: 24rpx;
|
||||
color: #6A7A99;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.secondary-entry-arrow {
|
||||
color: $brand-primary;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.register-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4rpx;
|
||||
background: $brand-primary;
|
||||
border-radius: 2rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(40rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
display: inline-block;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
1739
pkgAccount/register/register.vue
Normal file
1739
pkgAccount/register/register.vue
Normal file
File diff suppressed because it is too large
Load Diff
480
pkgAccount/registration_records/registration_records.vue
Normal file
480
pkgAccount/registration_records/registration_records.vue
Normal file
@@ -0,0 +1,480 @@
|
||||
<template>
|
||||
<view class="registration-records-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="page-main tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '48rpx' }">
|
||||
<scroll-view class="filter-scroll" scroll-x :show-scrollbar="false">
|
||||
<view class="filter-row tn-margin-bottom-lg">
|
||||
<view
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
class="filter-chip"
|
||||
:class="{ 'filter-chip--active': approvalStatus === item.value }"
|
||||
@click="changeStatus(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="providerUnavailable" class="empty-panel tn-padding-lg">
|
||||
<view class="empty-title">{{ t('providerUnavailableTitle') }}</view>
|
||||
<view class="empty-desc tn-margin-top-sm">{{ t('providerUnavailableDesc') }}</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="list.length">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="getRecordKey(item)"
|
||||
class="record-card tn-margin-bottom-lg"
|
||||
@click="goToRecordDetail(item)"
|
||||
>
|
||||
<view class="card-head tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="card-title-wrap">
|
||||
<view class="card-title">{{ item.name || item.account || t('registrationFallbackTitle') }}</view>
|
||||
<view class="card-code tn-margin-top-xs">{{ item.code || '--' }}</view>
|
||||
</view>
|
||||
<view :class="['status-badge', 'status-' + resolveStatusType(item.approvalStatus)]">
|
||||
{{ getDisplayStatusText(item) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-body tn-margin-top">
|
||||
<view class="info-row tn-flex tn-flex-row-between">
|
||||
<text class="info-label">{{ t('accountLabel') }}</text>
|
||||
<text class="info-value">{{ item.account || '--' }}</text>
|
||||
</view>
|
||||
<view class="info-row tn-flex tn-flex-row-between">
|
||||
<text class="info-label">{{ t('phoneLabel') }}</text>
|
||||
<text class="info-value">{{ item.phone || '--' }}</text>
|
||||
</view>
|
||||
<view class="info-row tn-flex tn-flex-row-between">
|
||||
<text class="info-label">{{ t('idNumberLabel') }}</text>
|
||||
<text class="info-value">{{ item.idCard || item.passportNumber || '--' }}</text>
|
||||
</view>
|
||||
<view class="info-row tn-flex tn-flex-row-between">
|
||||
<text class="info-label">{{ t('submittedAtLabel') }}</text>
|
||||
<text class="info-value">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.approveTime" class="info-row tn-flex tn-flex-row-between">
|
||||
<text class="info-label">{{ t('approvedAtLabel') }}</text>
|
||||
<text class="info-value">{{ formatDateTime(item.approveTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.approveReason" class="remark-panel tn-margin-top">
|
||||
<view class="remark-label">{{ item.approvalStatus === -1 ? t('rejectReasonLabel') : t('approvalRemarkLabel') }}</view>
|
||||
<view class="remark-text tn-margin-top-xs">{{ item.approveReason }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="load-state">
|
||||
<text v-if="loadingMore">{{ t('loadingMore') }}</text>
|
||||
<text v-else-if="!hasMore">{{ t('noMore') }}</text>
|
||||
<text v-else>{{ t('pullUpMore') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!loading" class="empty-panel tn-padding-lg">
|
||||
<view class="empty-title">{{ t('emptyTitle') }}</view>
|
||||
<view class="empty-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
|
||||
<view class="action-btn tn-margin-top-lg" @click="goToRegister">
|
||||
<text class="tn-text-bold">{{ t('goRegister') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !list.length && !providerUnavailable" class="empty-panel tn-padding-lg">
|
||||
<view class="empty-title">{{ t('loadingTitle') }}</view>
|
||||
<view class="empty-desc tn-margin-top-sm">{{ t('loadingDesc') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { registrationApplicationMePage } from '@/api/index.js'
|
||||
|
||||
function formatPageResponse(response) {
|
||||
const payload = response && response.data ? response.data : response
|
||||
return payload && typeof payload === 'object' ? payload : {}
|
||||
}
|
||||
|
||||
function normalizeRecordList(list = []) {
|
||||
if (!Array.isArray(list)) {
|
||||
return []
|
||||
}
|
||||
return list.filter(item => item && typeof item === 'object')
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
providerValue: '',
|
||||
providerUnavailable: false,
|
||||
approvalStatus: '',
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
list: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
statusOptions: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.syncStatusOptions()
|
||||
this.syncNavigationTitle()
|
||||
},
|
||||
onShow() {
|
||||
this.syncNavigationTitle()
|
||||
this.reloadList()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.reloadList().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
onReachBottom() {
|
||||
this.loadMore()
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
language() {
|
||||
this.syncNavigationTitle()
|
||||
this.syncStatusOptions()
|
||||
this.reloadList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgAccount.registration_records', key, params)
|
||||
},
|
||||
syncStatusOptions() {
|
||||
this.statusOptions = [
|
||||
{ label: this.t('statusAll'), value: '' },
|
||||
{ label: this.t('statusPending'), value: 0 },
|
||||
{ label: this.t('statusApproved'), value: 1 },
|
||||
{ label: this.t('statusRejected'), value: -1 }
|
||||
]
|
||||
},
|
||||
syncNavigationTitle() {
|
||||
this.$syncNavigationTitle()
|
||||
},
|
||||
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
|
||||
})
|
||||
},
|
||||
getStatusText(status) {
|
||||
const map = {
|
||||
0: this.t('statusPending'),
|
||||
1: this.t('statusApproved'),
|
||||
'-1': this.t('statusRejected')
|
||||
}
|
||||
return map[status] || this.t('statusProcessing')
|
||||
},
|
||||
getDisplayStatusText(item = {}) {
|
||||
if (this.language !== 'en' && item.approvalStatusDesc) {
|
||||
return item.approvalStatusDesc
|
||||
}
|
||||
return this.getStatusText(item.approvalStatus)
|
||||
},
|
||||
resolveStatusType(status) {
|
||||
if (Number(status) === 1) {
|
||||
return 'approved'
|
||||
}
|
||||
if (Number(status) === -1) {
|
||||
return 'rejected'
|
||||
}
|
||||
return 'pending'
|
||||
},
|
||||
formatDateTime(value) {
|
||||
if (!value) {
|
||||
return '--'
|
||||
}
|
||||
const text = String(value).replace('T', ' ')
|
||||
return text.length > 16 ? text.slice(0, 16) : text
|
||||
},
|
||||
getRecordKey(item = {}) {
|
||||
return item.code || `${item.account || 'registration'}-${item.createTime || ''}`
|
||||
},
|
||||
buildRecordRoutePayload(item = {}) {
|
||||
return {
|
||||
code: item.code || '',
|
||||
account: item.account || '',
|
||||
name: item.name || '',
|
||||
gender: typeof item.gender === 'number' ? item.gender : null,
|
||||
genderDesc: item.genderDesc || '',
|
||||
email: item.email || '',
|
||||
phone: item.phone || '',
|
||||
country: item.country || '',
|
||||
idCard: item.idCard || '',
|
||||
workUnit: item.workUnit || '',
|
||||
passportNumber: item.passportNumber || '',
|
||||
school: item.school || '',
|
||||
hobbyTags: item.hobbyTags || [],
|
||||
approvalStatus: typeof item.approvalStatus === 'number' ? item.approvalStatus : undefined,
|
||||
approvalStatusDesc: item.approvalStatusDesc || '',
|
||||
approveReason: item.approveReason || '',
|
||||
approveTime: item.approveTime || '',
|
||||
createTime: item.createTime || ''
|
||||
}
|
||||
},
|
||||
async ensureProviderValue() {
|
||||
const providerValue = await this.getWechatCode()
|
||||
this.providerValue = providerValue || ''
|
||||
this.providerUnavailable = !this.providerValue
|
||||
return this.providerValue
|
||||
},
|
||||
async reloadList() {
|
||||
this.pageIndex = 1
|
||||
this.hasMore = true
|
||||
this.list = []
|
||||
|
||||
const providerValue = await this.ensureProviderValue()
|
||||
if (!providerValue) {
|
||||
this.total = 0
|
||||
return
|
||||
}
|
||||
|
||||
await this.fetchList(true)
|
||||
},
|
||||
async loadMore() {
|
||||
if (!this.hasMore || this.loading || this.loadingMore || this.providerUnavailable) {
|
||||
return
|
||||
}
|
||||
this.pageIndex += 1
|
||||
await this.fetchList(false)
|
||||
},
|
||||
async fetchList(isReload = false) {
|
||||
if (!this.providerValue) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isReload) {
|
||||
this.loading = true
|
||||
} else {
|
||||
this.loadingMore = true
|
||||
}
|
||||
|
||||
const params = {
|
||||
ProviderValue: this.providerValue,
|
||||
PageIndex: this.pageIndex,
|
||||
PageSize: this.pageSize
|
||||
}
|
||||
if (this.approvalStatus !== '' && this.approvalStatus !== null && typeof this.approvalStatus !== 'undefined') {
|
||||
params.ApprovalStatus = this.approvalStatus
|
||||
}
|
||||
|
||||
await registrationApplicationMePage(params, this.language)
|
||||
.then((response) => {
|
||||
const pageData = formatPageResponse(response)
|
||||
const nextList = normalizeRecordList(pageData.data || [])
|
||||
const total = Number(pageData.total || 0)
|
||||
|
||||
this.total = total
|
||||
this.list = isReload ? nextList : this.list.concat(nextList)
|
||||
this.hasMore = this.list.length < total && nextList.length > 0
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!isReload) {
|
||||
this.pageIndex = Math.max(1, this.pageIndex - 1)
|
||||
}
|
||||
uni.showToast({
|
||||
title: (error && (error.message || error.msg)) || this.t('loadError'),
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
this.loadingMore = false
|
||||
})
|
||||
},
|
||||
changeStatus(value) {
|
||||
if (this.approvalStatus === value) {
|
||||
return
|
||||
}
|
||||
this.approvalStatus = value
|
||||
this.reloadList()
|
||||
},
|
||||
goToRecordDetail(item = {}) {
|
||||
const code = item.code || ''
|
||||
const payload = this.buildRecordRoutePayload(item)
|
||||
uni.navigateTo({
|
||||
url: `/pkgAccount/auth_result/auth_result?from=registration-records&code=${encodeURIComponent(code)}&payload=${encodeURIComponent(JSON.stringify(payload))}`
|
||||
})
|
||||
},
|
||||
goToRegister() {
|
||||
uni.navigateTo({
|
||||
url: '/pkgAccount/register/register'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.registration-records-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
}
|
||||
|
||||
.record-card,
|
||||
.empty-panel {
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid rgba($brand-primary, 0.05);
|
||||
}
|
||||
.record-card:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.empty-title,
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1A2233;
|
||||
}
|
||||
|
||||
.empty-desc,
|
||||
.card-code,
|
||||
.info-label,
|
||||
.remark-label,
|
||||
.load-state {
|
||||
color: #6A7A99;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 16rpx;
|
||||
padding: 4rpx;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
flex-shrink: 0;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #FFFFFF;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
color: #6A7A99;
|
||||
font-size: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.05);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-chip--active {
|
||||
background: #FFFFFF;
|
||||
border-color: $brand-primary;
|
||||
color: $brand-primary;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.1);
|
||||
}
|
||||
|
||||
.card-code {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
color: #B45309;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background: rgba($brand-primary, 0.12);
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #B91C1C;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1px dashed rgba($brand-primary, 0.15);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
max-width: 62%;
|
||||
text-align: right;
|
||||
color: #1A2233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.remark-panel {
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba($brand-primary, 0.02);
|
||||
border: 1rpx solid rgba($brand-primary, 0.1);
|
||||
}
|
||||
|
||||
.remark-text {
|
||||
color: #1A2233;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999rpx;
|
||||
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);
|
||||
}
|
||||
|
||||
.load-state {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
padding: 12rpx 0 24rpx;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user