|
|
@@ -0,0 +1,583 @@
|
|
|
+<template>
|
|
|
+ <view class="bank-auth">
|
|
|
+ <scroll-view scroll-y class="bank-auth__scroll">
|
|
|
+ <view class="bank-auth__hero">
|
|
|
+ <view class="bank-auth__hero-main">
|
|
|
+ <text class="bank-auth__hero-title">银行卡认证</text>
|
|
|
+ <text class="bank-auth__hero-desc">
|
|
|
+ 请绑定本人 I 类银行账户借记卡,用于佣金结算发放
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="bank-auth__hero-badge">
|
|
|
+ {{ certified ? '已认证' : '待认证' }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="notice-card">
|
|
|
+ <view class="notice-card__icon">!</view>
|
|
|
+ <text class="notice-card__text"> 需绑定本人 I 类银行账户借记卡,否则无法发放佣金。 </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-card">
|
|
|
+ <view class="form-header">
|
|
|
+ <text class="form-title">银行卡信息</text>
|
|
|
+ <text class="form-subtitle">请确认银行卡开户人与身份认证信息一致</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-row">
|
|
|
+ <text class="form-label">银行卡号</text>
|
|
|
+ <input
|
|
|
+ v-model="bankInfo.cardNumber"
|
|
|
+ class="form-input"
|
|
|
+ type="number"
|
|
|
+ maxlength="19"
|
|
|
+ placeholder="请输入银行卡号"
|
|
|
+ placeholder-class="form-placeholder"
|
|
|
+ :disabled="isFormDisabled"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-row">
|
|
|
+ <text class="form-label">开户行</text>
|
|
|
+ <input
|
|
|
+ v-model="bankInfo.bankName"
|
|
|
+ class="form-input"
|
|
|
+ placeholder="请输入开户行"
|
|
|
+ placeholder-class="form-placeholder"
|
|
|
+ :disabled="isFormDisabled"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-row">
|
|
|
+ <text class="form-label">预留手机号</text>
|
|
|
+ <input
|
|
|
+ v-model="bankInfo.phone"
|
|
|
+ class="form-input"
|
|
|
+ type="number"
|
|
|
+ maxlength="11"
|
|
|
+ placeholder="请输入银行预留手机号"
|
|
|
+ placeholder-class="form-placeholder"
|
|
|
+ :disabled="isFormDisabled"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+
|
|
|
+ <view class="bottom-bar">
|
|
|
+ <button class="bottom-btn bottom-btn--secondary" :disabled="loading" @click="handleBack">
|
|
|
+ 返回
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button
|
|
|
+ class="bottom-btn bottom-btn--primary"
|
|
|
+ :disabled="loading || certified"
|
|
|
+ @click="handleSubmit"
|
|
|
+ >
|
|
|
+ {{ submitButtonText }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <BaseAuthDialog
|
|
|
+ v-model="bankPhoneAuthFailVisible"
|
|
|
+ title="手机三要素认证未通过"
|
|
|
+ confirm-text="我知道啦"
|
|
|
+ >
|
|
|
+ <view class="dialog-text"> 抱歉,您的预留手机号三要素认证未通过! </view>
|
|
|
+
|
|
|
+ <view class="dialog-text"> 请确保您的手机号持卡人身份证号与您提供的身份证号一致: </view>
|
|
|
+
|
|
|
+ <view class="dialog-text">
|
|
|
+ 您可以登录银行卡 APP 或前往网点
|
|
|
+ <text class="dialog-highlight">更换预留手机号</text>
|
|
|
+ 。
|
|
|
+ </view>
|
|
|
+ </BaseAuthDialog>
|
|
|
+
|
|
|
+ <BaseAuthDialog
|
|
|
+ v-model="reSignVisible"
|
|
|
+ title="重新签约提醒"
|
|
|
+ confirm-text="去签约"
|
|
|
+ @confirm="handleGoSign"
|
|
|
+ >
|
|
|
+ <view class="dialog-title-text"> 您更新了银行卡信息,需要与结算渠道重新签约。 </view>
|
|
|
+
|
|
|
+ <view class="dialog-text">
|
|
|
+ 请点击“去签约”完成结算渠道签约,在完成签约前将无法完成结算。
|
|
|
+ </view>
|
|
|
+ </BaseAuthDialog>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, reactive, ref } from 'vue'
|
|
|
+
|
|
|
+import { onLoad, onUnload } from '@dcloudio/uni-app'
|
|
|
+
|
|
|
+import {
|
|
|
+ bindBankInfoApi,
|
|
|
+ gigCertPersonVerifyMobile3Api,
|
|
|
+ personVerifyBank4Api,
|
|
|
+} from '@/services/modules/auth'
|
|
|
+
|
|
|
+import { useUserStore } from '@/stores/modules/user'
|
|
|
+
|
|
|
+import BaseAuthDialog from '@/components/BaseAuthDialog/index.vue'
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: 'BankAuthPage',
|
|
|
+})
|
|
|
+
|
|
|
+const API_SUCCESS_CODE = 0
|
|
|
+const SIGN_URL = '/pages-sub-packages/mine/auth/index'
|
|
|
+const BACK_DELAY = 500
|
|
|
+
|
|
|
+interface CurrentUserForBankAuth {
|
|
|
+ phone?: string
|
|
|
+ realname?: string
|
|
|
+ idCardNumber?: string
|
|
|
+ bankCardNumber?: string
|
|
|
+ bankPhone?: string
|
|
|
+}
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const certified = ref(false)
|
|
|
+const bankPhoneAuthFailVisible = ref(false)
|
|
|
+const reSignVisible = ref(false)
|
|
|
+
|
|
|
+let backTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
+
|
|
|
+const bankInfo = reactive({
|
|
|
+ cardNumber: '',
|
|
|
+ bankName: '',
|
|
|
+ phone: '',
|
|
|
+})
|
|
|
+
|
|
|
+const currentUserInfo = computed(() => {
|
|
|
+ return (userStore.currentUserInfo ?? {}) as CurrentUserForBankAuth
|
|
|
+})
|
|
|
+
|
|
|
+const loginMobile = computed(() => currentUserInfo.value.phone ?? '')
|
|
|
+const realName = computed(() => currentUserInfo.value.realname ?? '')
|
|
|
+const idCardNumber = computed(() => currentUserInfo.value.idCardNumber ?? '')
|
|
|
+
|
|
|
+const hasExistingBankInfo = computed(() => {
|
|
|
+ return Boolean(currentUserInfo.value.bankCardNumber || currentUserInfo.value.bankPhone)
|
|
|
+})
|
|
|
+
|
|
|
+const isFormDisabled = computed(() => loading.value || certified.value)
|
|
|
+
|
|
|
+const submitButtonText = computed(() => {
|
|
|
+ if (certified.value) return '已认证'
|
|
|
+ if (loading.value) return '提交中...'
|
|
|
+ return '去认证'
|
|
|
+})
|
|
|
+
|
|
|
+const resetForm = () => {
|
|
|
+ certified.value = false
|
|
|
+ bankPhoneAuthFailVisible.value = false
|
|
|
+ reSignVisible.value = false
|
|
|
+
|
|
|
+ bankInfo.cardNumber = ''
|
|
|
+ bankInfo.bankName = ''
|
|
|
+ bankInfo.phone = ''
|
|
|
+}
|
|
|
+
|
|
|
+const normalizeForm = () => {
|
|
|
+ bankInfo.cardNumber = bankInfo.cardNumber.replace(/\s+/g, '')
|
|
|
+ bankInfo.bankName = bankInfo.bankName.trim()
|
|
|
+ bankInfo.phone = bankInfo.phone.replace(/\s+/g, '')
|
|
|
+}
|
|
|
+
|
|
|
+const validateUserIdentity = () => {
|
|
|
+ if (!realName.value || !idCardNumber.value) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请先完成身份信息认证',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const validateForm = () => {
|
|
|
+ normalizeForm()
|
|
|
+
|
|
|
+ if (!/^\d{16,19}$/.test(bankInfo.cardNumber)) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '银行卡号格式错误',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (bankInfo.bankName.length < 2) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '开户行名称过短',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!/^1[3-9]\d{9}$/.test(bankInfo.phone)) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '手机号格式错误',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const verifyBankPhoneIfNeeded = async () => {
|
|
|
+ if (loginMobile.value === bankInfo.phone) return true
|
|
|
+
|
|
|
+ const res = await gigCertPersonVerifyMobile3Api({
|
|
|
+ mobile: bankInfo.phone,
|
|
|
+ realName: realName.value,
|
|
|
+ cardId: idCardNumber.value,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (res.code !== API_SUCCESS_CODE) {
|
|
|
+ bankPhoneAuthFailVisible.value = true
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const verifyBankCard = async () => {
|
|
|
+ const res = await personVerifyBank4Api({
|
|
|
+ mobile: bankInfo.phone,
|
|
|
+ realName: realName.value,
|
|
|
+ cardId: idCardNumber.value,
|
|
|
+ bankCard: bankInfo.cardNumber,
|
|
|
+ bankPhone: bankInfo.phone,
|
|
|
+ bankName: bankInfo.bankName,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (res.code !== API_SUCCESS_CODE) {
|
|
|
+ uni.showToast({
|
|
|
+ title: res.msg || '认证失败',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const bindBankInfo = async () => {
|
|
|
+ const res = await bindBankInfoApi({
|
|
|
+ bankCardNumber: bankInfo.cardNumber,
|
|
|
+ bankPhone: bankInfo.phone,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (res.code !== API_SUCCESS_CODE) {
|
|
|
+ uni.showToast({
|
|
|
+ title: res.msg || '银行卡绑定失败',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const handleSubmit = async () => {
|
|
|
+ if (loading.value || certified.value) return
|
|
|
+ if (!validateUserIdentity() || !validateForm()) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ loading.value = true
|
|
|
+
|
|
|
+ const bankPhonePassed = await verifyBankPhoneIfNeeded()
|
|
|
+ if (!bankPhonePassed) return
|
|
|
+
|
|
|
+ const bankCardPassed = await verifyBankCard()
|
|
|
+ if (!bankCardPassed) return
|
|
|
+
|
|
|
+ const bindPassed = await bindBankInfo()
|
|
|
+ if (!bindPassed) return
|
|
|
+
|
|
|
+ certified.value = true
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '认证成功',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+
|
|
|
+ if (hasExistingBankInfo.value) {
|
|
|
+ reSignVisible.value = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ await userStore.getUserInfoByCode()
|
|
|
+
|
|
|
+ backTimer = setTimeout(() => {
|
|
|
+ uni.navigateBack()
|
|
|
+ }, BACK_DELAY)
|
|
|
+ } catch {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请求失败,请检查网络',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleGoSign = () => {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: SIGN_URL,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleBack = () => {
|
|
|
+ if (loading.value) return
|
|
|
+ uni.navigateBack()
|
|
|
+}
|
|
|
+
|
|
|
+onLoad(() => {
|
|
|
+ resetForm()
|
|
|
+})
|
|
|
+
|
|
|
+onUnload(() => {
|
|
|
+ if (backTimer) {
|
|
|
+ clearTimeout(backTimer)
|
|
|
+ backTimer = null
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.bank-auth {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(180deg, #eef6ff 0%, #f7f8fa 420rpx), #f7f8fa;
|
|
|
+}
|
|
|
+
|
|
|
+.bank-auth__scroll {
|
|
|
+ height: 100vh;
|
|
|
+ padding: 28rpx 28rpx 180rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.bank-auth__hero {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ padding: 34rpx 32rpx;
|
|
|
+ border-radius: 28rpx;
|
|
|
+ background: linear-gradient(135deg, #1677ff 0%, #69b1ff 100%);
|
|
|
+ box-shadow: 0 18rpx 40rpx rgba(22, 119, 255, 0.18);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.bank-auth__hero-main {
|
|
|
+ flex: 1;
|
|
|
+ padding-right: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.bank-auth__hero-title {
|
|
|
+ display: block;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 40rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 56rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.bank-auth__hero-desc {
|
|
|
+ display: block;
|
|
|
+ margin-top: 12rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.88);
|
|
|
+ font-size: 26rpx;
|
|
|
+ line-height: 38rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.bank-auth__hero-badge {
|
|
|
+ flex-shrink: 0;
|
|
|
+ height: 48rpx;
|
|
|
+ padding: 0 18rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.18);
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 48rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.notice-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ padding: 24rpx 26rpx;
|
|
|
+ border: 1rpx solid #ffe3b8;
|
|
|
+ border-radius: 22rpx;
|
|
|
+ background: #fff8ec;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.notice-card__icon {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 34rpx;
|
|
|
+ height: 34rpx;
|
|
|
+ margin-top: 2rpx;
|
|
|
+ margin-right: 14rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #fa8c16;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 24rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 34rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.notice-card__text {
|
|
|
+ flex: 1;
|
|
|
+ color: #b45309;
|
|
|
+ font-size: 26rpx;
|
|
|
+ line-height: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-card {
|
|
|
+ padding: 32rpx;
|
|
|
+ border-radius: 28rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ box-shadow: 0 10rpx 32rpx rgba(15, 23, 42, 0.06);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.form-header {
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-title {
|
|
|
+ display: block;
|
|
|
+ color: #111827;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 48rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-subtitle {
|
|
|
+ display: block;
|
|
|
+ margin-top: 8rpx;
|
|
|
+ color: #8a94a6;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 34rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 104rpx;
|
|
|
+ border-bottom: 1rpx solid #eef0f3;
|
|
|
+}
|
|
|
+
|
|
|
+.form-row:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.form-label {
|
|
|
+ width: 170rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+ color: #4b5563;
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-input {
|
|
|
+ flex: 1;
|
|
|
+ height: 72rpx;
|
|
|
+ color: #111827;
|
|
|
+ font-size: 29rpx;
|
|
|
+ line-height: 72rpx;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.form-placeholder {
|
|
|
+ color: #b6beca;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-bar {
|
|
|
+ position: fixed;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ z-index: 20;
|
|
|
+ display: flex;
|
|
|
+ gap: 20rpx;
|
|
|
+ padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
|
|
|
+ border-top: 1rpx solid rgba(229, 231, 235, 0.8);
|
|
|
+ background: rgba(255, 255, 255, 0.96);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 88rpx;
|
|
|
+ margin: 0;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 88rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn--secondary {
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: #4b5563;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn--primary {
|
|
|
+ background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
|
|
|
+ color: #ffffff;
|
|
|
+ box-shadow: 0 12rpx 28rpx rgba(22, 119, 255, 0.22);
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn[disabled] {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn--secondary[disabled] {
|
|
|
+ background: #f3f4f6 !important;
|
|
|
+ color: #a1a1aa !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-btn--primary[disabled] {
|
|
|
+ background: #d1d5db !important;
|
|
|
+ color: #ffffff !important;
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-title-text {
|
|
|
+ margin-bottom: 32rpx;
|
|
|
+ color: #111827;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 48rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-text {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ color: #4b5563;
|
|
|
+ font-size: 29rpx;
|
|
|
+ line-height: 46rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-text:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-highlight {
|
|
|
+ color: #1677ff;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+</style>
|