Files
medical-mall/pages/mall/consumer/bank-cards/verify.uvue
2026-05-14 15:28:09 +08:00

559 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 身份验证页(绑定银行卡 Step 2 -->
<view class="page">
<scroll-view direction="vertical" class="scroll-wrap">
<!-- ① 银行卡信息卡片 -->
<view class="bank-info-card">
<view class="bank-logo-circle">
<text class="bank-logo-text">{{ bankShortName }}</text>
</view>
<view class="bank-info-body">
<text class="bank-info-name">{{ bankDisplayName }}</text>
<text class="bank-info-no">{{ maskedCardNo }}</text>
</view>
</view>
<!-- ② 验证方式 Tab -->
<view class="verify-method-tabs">
<view class="method-tab method-tab-active">
<text class="method-tab-text method-tab-text-active">短信验证</text>
</view>
<view class="method-tab method-tab-disabled">
<text class="method-tab-text method-tab-text-disabled">转账验证(暂不支持)</text>
</view>
</view>
<!-- ③ 验证表单白卡 -->
<view class="form-card">
<!-- 手机号 + 获取验证码 -->
<view class="form-row">
<view class="row-left">
<text class="row-label">手机号</text>
</view>
<view class="row-right">
<text class="phone-display">{{ maskedPhone }}</text>
</view>
</view>
<view class="divider" />
<!-- 验证码 -->
<view class="form-row">
<view class="row-left">
<text class="row-label">验证码</text>
</view>
<view class="row-right">
<input
class="row-input"
type="number"
v-model="smsCode"
placeholder="请输入验证码"
placeholder-style="color:#c0c4cc;font-size:28rpx"
:maxlength="6"
/>
<view
class="sms-btn"
:class="countdown > 0 ? 'sms-btn-disabled' : 'sms-btn-active'"
@click="getSmsCode"
>
<text class="sms-btn-text">{{ countdown > 0 ? countdown + 's后重发' : '获取验证码' }}</text>
</view>
</view>
</view>
</view>
<!-- ④ 底部说明 -->
<view class="desc-area">
<text class="desc-text">
验证码将发送至银行预留手机号 {{ maskedPhone }}。若手机号有误,请返回修改。
</text>
</view>
<view class="bottom-placeholder" />
</scroll-view>
<!-- ⑤ 底部操作栏 -->
<view class="bottom-bar">
<view
class="confirm-btn"
:class="canConfirm ? 'confirm-btn-active' : 'confirm-btn-disabled'"
@click="doVerifyAndBind"
>
<text class="confirm-btn-text">{{ submitting ? '绑定中...' : '验证并绑定' }}</text>
</view>
<view class="back-modify" @click="goBack">
<text class="back-modify-text">返回修改</text>
</view>
<view style="height: env(safe-area-inset-bottom); min-height: 8rpx;" />
</view>
</view>
</template>
<script lang="uts">
import {
maskCardNo,
isValidPhone,
sendSmsCode,
verifySmsCode
} from '@/utils/bankUtils.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
type CardParams = {
holder_name: string
card_no: string
bank_name: string
bank_code: string
branch_name: string
phone: string
}
export default {
data() {
return {
cardParams: {
holder_name: '',
card_no: '',
bank_name: '',
bank_code: '',
branch_name: '',
phone: ''
} as CardParams,
smsCode: '' as string,
countdown: 0 as number,
countdownTimer: null as number | null,
submitting: false as boolean
}
},
computed: {
maskedCardNo(): string {
const cardNo = this.cardParams.card_no != null ? this.cardParams.card_no : ''
return maskCardNo(cardNo)
},
maskedPhone(): string {
const p = this.cardParams.phone != null ? this.cardParams.phone : ''
if (p.length < 7) return p
return p.substring(0, 3) + '****' + p.substring(7)
},
bankShortName(): string {
const name = this.cardParams.bank_name != null ? this.cardParams.bank_name : ''
if (name === '') return '银行'
// 取前两个汉字作为 logo 占位文字
return name.length >= 2 ? name.substring(0, 2) : name
},
bankDisplayName(): string {
return this.cardParams.bank_name != null ? this.cardParams.bank_name : ''
},
canConfirm(): boolean {
return this.smsCode.length === 6 && !this.submitting
}
},
onLoad(options) {
if (options == null) {
return
}
const optionsObj = options as UTSJSONObject
const dataStr = optionsObj.getString('data') ?? ''
if (dataStr != '') {
try {
const decodedValue = decodeURIComponent(dataStr)
const decoded = decodedValue != null ? decodedValue.toString() : dataStr
const parsedObj = JSON.parse(decoded) as UTSJSONObject
this.cardParams = {
holder_name: parsedObj.getString('holder_name') ?? '',
card_no: parsedObj.getString('card_no') ?? '',
bank_name: parsedObj.getString('bank_name') ?? '',
bank_code: parsedObj.getString('bank_code') ?? '',
branch_name: parsedObj.getString('branch_name') ?? '',
phone: parsedObj.getString('phone') ?? ''
} as CardParams
} catch (e) {
console.error('[verify] 解析参数失败:', e)
uni.showToast({ title: '页面参数异常', icon: 'none' })
}
}
},
onUnmounted() {
this.clearCountdown()
},
methods: {
// ── 获取验证码 ─────────────────────────────
async getSmsCode() {
if (this.countdown > 0) return
if (!isValidPhone(this.cardParams.phone)) {
uni.showToast({ title: '手机号格式有误', icon: 'none' })
return
}
uni.showLoading({ title: '发送中...' })
try {
const result = await sendSmsCode(this.cardParams.phone)
uni.hideLoading()
if (result.success === true) {
uni.showToast({ title: '验证码已发送', icon: 'success' })
this.startCountdown(60)
} else {
uni.showToast({ title: (result.errorMsg != null && result.errorMsg !== '' ? result.errorMsg : '发送失败'), icon: 'none' })
}
} catch (e) {
uni.hideLoading()
console.error('[verify] 发送验证码异常:', e)
uni.showToast({ title: '发送异常,请重试', icon: 'none' })
}
},
startCountdown(seconds: number) {
this.countdown = seconds
this.countdownTimer = setInterval(() => {
if (this.countdown > 0) {
this.countdown--
} else {
this.clearCountdown()
}
}, 1000) as number
},
clearCountdown() {
if (this.countdownTimer != null) {
clearInterval(this.countdownTimer!)
this.countdownTimer = null
}
},
// ── 验证并绑定 ─────────────────────────────
async doVerifyAndBind() {
if (!this.canConfirm) {
if (this.smsCode.length < 6) {
uni.showToast({ title: '请输入6位验证码', icon: 'none' })
}
return
}
this.submitting = true
uni.showLoading({ title: '验证中...' })
try {
// 1. 校验验证码
const verifyResult = await verifySmsCode(this.cardParams.phone, this.smsCode)
if (verifyResult.success !== true) {
uni.hideLoading()
uni.showToast({ title: (verifyResult.errorMsg != null && verifyResult.errorMsg !== '' ? verifyResult.errorMsg : '验证码错误'), icon: 'none' })
this.submitting = false
return
}
// 2. 提交绑卡到数据库
// 只写入 ml_user_bank_cards 表中已确认存在的列:
// holder_name / bank_name / card_no_last4 / phone / card_type / is_default
// 扩展字段bank_code / branch_name / verified_status / verified_at
// 待数据库执行对应 ALTER TABLE 后再取消注释
const rawCardNo = this.cardParams.card_no
const last4 = rawCardNo.length > 4 ? rawCardNo.substring(rawCardNo.length - 4) : rawCardNo
const cardData = new UTSJSONObject()
cardData.set('holder_name', this.cardParams.holder_name)
cardData.set('bank_name', this.cardParams.bank_name)
cardData.set('card_no_last4', last4)
cardData.set('phone', this.cardParams.phone)
cardData.set('card_type', 'debit')
cardData.set('is_default', false)
// ── 扩展字段(取消注释前请先执行 SQL 添加对应列) ──
// cardData.set('bank_code', this.cardParams.bank_code || '')
// if (this.cardParams.branch_name) {
// cardData.set('branch_name', this.cardParams.branch_name)
// }
// cardData.set('verified_status', 'verified')
// cardData.set('verified_at', new Date().toISOString())
const success = await supabaseService.addBankCard(cardData)
uni.hideLoading()
if (success) {
// 写入信号:让 finance-management 的 onShow 能同步读取并立即更新 UI
// 避免因异步 DB 查询时序问题导致页面返回时看到旧状态
uni.setStorageSync('__bank_just_bound__', '1')
uni.setStorageSync('__bound_bank_name__', this.cardParams.bank_name)
uni.setStorageSync('__bound_card_last4__', last4)
uni.showToast({ title: '绑定成功', icon: 'success' })
setTimeout(() => {
// 返回银行卡列表页,并跳过 add 页(跳 2 层)
const pages = getCurrentPages()
if (pages.length >= 2) {
uni.navigateBack({ delta: 2 })
} else {
uni.navigateBack()
}
}, 1200)
} else {
uni.showToast({ title: '绑定失败,请重试', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
console.error('[verify] 绑定异常:', e)
uni.showToast({ title: '系统异常,请重试', icon: 'none' })
} finally {
this.submitting = false
}
},
// ── 返回修改 ───────────────────────────────
goBack() {
uni.navigateBack()
}
}
}
</script>
<style>
/* ── 整体结构 ────────────────────────────── */
.page {
flex: 1;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.scroll-wrap {
flex: 1;
}
/* ── 银行卡信息卡片 ──────────────────────── */
.bank-info-card {
background-color: #ffffff;
margin: 24rpx 24rpx 0;
border-radius: 16rpx;
padding: 32rpx 28rpx;
display: flex;
flex-direction: row;
align-items: center;
}
.bank-logo-circle {
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
background-color: #e23636;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
}
.bank-logo-text {
font-size: 26rpx;
color: #ffffff;
font-weight: bold;
}
.bank-info-body {
flex: 1;
display: flex;
flex-direction: column;
}
.bank-info-name {
font-size: 30rpx;
color: #222222;
font-weight: 600;
margin-bottom: 8rpx;
}
.bank-info-no {
font-size: 26rpx;
color: #888888;
letter-spacing: 2rpx;
}
/* ── 验证方式 Tabs ───────────────────────── */
.verify-method-tabs {
display: flex;
flex-direction: row;
margin: 20rpx 24rpx 0;
background-color: #f0f0f0;
border-radius: 12rpx;
padding: 6rpx;
}
.method-tab {
flex: 1;
height: 64rpx;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
}
.method-tab-active {
background-color: #ffffff;
}
.method-tab-disabled {
background-color: transparent;
}
.method-tab-text {
font-size: 26rpx;
}
.method-tab-text-active {
color: #e23636;
font-weight: 600;
}
.method-tab-text-disabled {
color: #aaaaaa;
}
/* ── 表单卡片 ────────────────────────────── */
.form-card {
background-color: #ffffff;
border-radius: 16rpx;
margin: 20rpx 24rpx 0;
overflow: hidden;
}
.form-row {
display: flex;
flex-direction: row;
align-items: center;
min-height: 112rpx;
padding: 0 28rpx;
}
.row-left {
width: 160rpx;
flex-shrink: 0;
}
.row-label {
font-size: 28rpx;
color: #222222;
font-weight: 500;
}
.row-right {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
}
.phone-display {
font-size: 28rpx;
color: #333333;
flex: 1;
}
.row-input {
flex: 1;
font-size: 28rpx;
color: #222222;
height: 80rpx;
line-height: 80rpx;
}
.divider {
height: 1rpx;
background-color: #f2f2f2;
margin-left: 28rpx;
}
/* ── 获取验证码按钮 ──────────────────────── */
.sms-btn {
height: 64rpx;
padding: 0 24rpx;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.sms-btn-active {
background-color: #e23636;
}
.sms-btn-disabled {
background-color: #f5a0a0;
}
.sms-btn-text {
font-size: 24rpx;
color: #ffffff;
white-space: nowrap;
}
/* ── 说明文字 ────────────────────────────── */
.desc-area {
padding: 24rpx 32rpx 0;
}
.desc-text {
font-size: 22rpx;
color: #aaaaaa;
line-height: 1.7;
}
.bottom-placeholder {
height: 200rpx;
}
/* ── 底部操作栏 ──────────────────────────── */
.bottom-bar {
background-color: #ffffff;
padding: 20rpx 32rpx 0;
border-top-width: 1rpx;
border-top-style: solid;
border-top-color: #f0f0f0;
}
.confirm-btn {
border-radius: 56rpx;
height: 96rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.confirm-btn-active {
background-color: #e23636;
}
.confirm-btn-disabled {
background-color: #f5a0a0;
}
.confirm-btn-text {
font-size: 32rpx;
color: #ffffff;
font-weight: 600;
}
.back-modify {
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-modify-text {
font-size: 28rpx;
color: #888888;
}
</style>