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

675 lines
17 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>
<!-- 绑定银行卡 - 添加信息页(拼多多风格) -->
<view class="page">
<scroll-view direction="vertical" class="scroll-wrap">
<!-- ① 提示卡片 -->
<view class="tip-banner">
<text class="tip-icon">🔒</text>
<text class="tip-text">银行卡信息经加密传输,安全有保障</text>
</view>
<!-- ② 表单白色卡片 -->
<view class="form-card">
<!-- 持卡人 -->
<view class="form-row">
<view class="row-left">
<text class="row-label">持卡人</text>
</view>
<view class="row-right">
<input
class="row-input"
type="text"
v-model="form.holder_name"
placeholder="请输入银行卡开户人姓名"
placeholder-style="color:#c0c4cc;font-size:28rpx"
:maxlength="20"
/>
<text class="row-tip-icon" @click="showHolderTip">ⓘ</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="cardNoDisplay"
placeholder="请输入银行卡号"
placeholder-style="color:#c0c4cc;font-size:28rpx"
:maxlength="23"
@input="onCardNoInput"
/>
<text class="camera-btn" @click="doOCR">📷</text>
</view>
</view>
<view class="divider" />
<!-- 开户银行(选择器) -->
<view class="form-row selector-row" @click="openBankPicker">
<view class="row-left">
<text class="row-label">开户银行</text>
</view>
<view class="row-right">
<view class="selector-value-wrap">
<text
class="selector-value"
:class="{ placeholder: form.bank_name === '' }"
>
{{ form.bank_name ?? '请选择开户银行' }}
</text>
</view>
<text class="arrow-icon"></text>
</view>
</view>
<view class="divider" />
<!-- 开户支行(仅选了银行后显示,非必填) -->
<view v-if="form.bank_name !== ''" class="form-row selector-row" @click="openBranchInput">
<view class="row-left">
<text class="row-label">开户支行</text>
<text class="optional-badge">非必填</text>
</view>
<view class="row-right">
<view class="selector-value-wrap">
<text
class="selector-value"
:class="{ placeholder: form.branch_name === '' }"
>
{{ form.branch_name ?? '请填写开户支行(可选)' }}
</text>
</view>
<text class="arrow-icon"></text>
</view>
</view>
<view v-if="form.bank_name !== ''" 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="form.phone"
placeholder="请输入该银行卡的预留手机号"
placeholder-style="color:#c0c4cc;font-size:28rpx"
:maxlength="11"
/>
</view>
</view>
</view>
<!-- ③ 安全说明 -->
<view class="desc-area">
<text class="desc-text">
手机号码将作为账户安全验证依据,请填写该银行卡在银行预留的手机号。
</text>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder" />
</scroll-view>
<!-- ④ 底部大按钮(固定) -->
<view class="bottom-bar">
<view
class="next-btn"
:class="canSubmit ? 'next-btn-active' : 'next-btn-disabled'"
@click="goVerify"
>
<text class="next-btn-text">下一步,身份验证</text>
</view>
<view style="height: env(safe-area-inset-bottom); min-height: 8rpx;" />
</view>
<!-- ⑤ 银行选择弹层 -->
<view v-if="bankPickerVisible" class="picker-mask" @click="closeBankPicker">
<view class="picker-panel" @click.stop>
<view class="picker-header">
<text class="picker-title">选择开户银行</text>
<text class="picker-close" @click="closeBankPicker">✕</text>
</view>
<scroll-view direction="vertical" class="picker-scroll">
<view
v-for="bank in bankList"
:key="bank.code"
class="picker-item"
:class="{ 'picker-item-active': form.bank_name === bank.name }"
@click="selectBank(bank)"
>
<view class="bank-logo-placeholder">
<text class="bank-logo-text">{{ bank.shortName.substring(0, 2) }}</text>
</view>
<text class="picker-item-name">{{ bank.name }}</text>
<text v-if="form.bank_name === bank.name" class="picker-check">✓</text>
</view>
</scroll-view>
</view>
</view>
<!-- ⑥ 开户支行输入弹层 -->
<view v-if="branchInputVisible" class="picker-mask" @click="closeBranchInput">
<view class="branch-panel" @click.stop>
<view class="picker-header">
<text class="picker-title">填写开户支行</text>
<text class="picker-close" @click="closeBranchInput">✕</text>
</view>
<view class="branch-input-wrap">
<input
class="branch-input"
type="text"
v-model="branchInputTemp"
placeholder="例:招商银行上海浦东支行"
placeholder-style="color:#c0c4cc"
:maxlength="50"
/>
</view>
<view class="branch-confirm-btn" @click="confirmBranch">
<text class="branch-confirm-text">确认</text>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import {
BANK_LIST,
BankItem,
detectBankByBin,
formatCardNo,
cleanCardNo,
luhnCheck,
isValidPhone,
recognizeBankCardFromImage
} from '@/utils/bankUtils.uts'
type AddCardForm = {
holder_name: string
card_no: string // 纯数字,提交时用
bank_name: string
bank_code: string
branch_name: string
phone: string
}
export default {
data() {
return {
form: {
holder_name: '',
card_no: '',
bank_name: '',
bank_code: '',
branch_name: '',
phone: ''
} as AddCardForm,
cardNoDisplay: '' as string, // 格式化显示(含空格)
bankList: BANK_LIST as BankItem[],
bankPickerVisible: false as boolean,
branchInputVisible: false as boolean,
branchInputTemp: '' as string,
ocrLoading: false as boolean
}
},
computed: {
canSubmit(): boolean {
return (
this.form.holder_name.trim().length > 0 &&
cleanCardNo(this.cardNoDisplay).length >= 16 &&
this.form.bank_name.length > 0 &&
isValidPhone(this.form.phone)
)
}
},
methods: {
// ── 持卡人提示 ──────────────────────────────
showHolderTip() {
uni.showToast({
title: '请填写银行卡开户人姓名,需与实名认证一致',
icon: 'none',
duration: 3000
})
},
// ── 卡号输入:格式化 + BIN 识别 ─────────────
onCardNoInput() {
const raw = cleanCardNo(this.cardNoDisplay)
// 重新格式化
this.cardNoDisplay = formatCardNo(raw)
this.form.card_no = raw
// BIN 识别(输入 6 位后触发)
if (raw.length >= 6) {
const bank = detectBankByBin(raw)
if (bank != null && this.form.bank_name === '') {
this.form.bank_name = bank.name
this.form.bank_code = bank.code
}
}
},
// ── OCR 识别 ────────────────────────────────
async doOCR() {
if (this.ocrLoading) return
this.ocrLoading = true
try {
const result = await recognizeBankCardFromImage()
if (result.success === true) {
if (result.cardNo != null && result.cardNo !== '') {
this.form.card_no = result.cardNo
this.cardNoDisplay = formatCardNo(result.cardNo)
}
if (result.bankName != null && result.bankName !== '' && this.form.bank_name === '') {
this.form.bank_name = result.bankName
}
uni.showToast({ title: '识别成功', icon: 'success' })
} else if (result.errorMsg !== '取消选择') {
uni.showToast({ title: (result.errorMsg != null && result.errorMsg !== '' ? result.errorMsg : 'OCR 识别失败'), icon: 'none' })
}
} catch (e) {
console.error('[add] OCR 异常:', e)
uni.showToast({ title: 'OCR 出现异常', icon: 'none' })
} finally {
this.ocrLoading = false
}
},
// ── 银行选择弹层 ─────────────────────────────
openBankPicker() {
this.bankPickerVisible = true
},
closeBankPicker() {
this.bankPickerVisible = false
},
selectBank(bank: BankItem) {
this.form.bank_name = bank.name
this.form.bank_code = bank.code
this.bankPickerVisible = false
},
// ── 支行输入弹层 ─────────────────────────────
openBranchInput() {
this.branchInputTemp = this.form.branch_name
this.branchInputVisible = true
},
closeBranchInput() {
this.branchInputVisible = false
},
confirmBranch() {
this.form.branch_name = this.branchInputTemp.trim()
this.branchInputVisible = false
},
// ── 下一步:跳转到身份验证页 ─────────────────
goVerify() {
if (!this.canSubmit) {
if (this.form.holder_name.trim() === '') {
uni.showToast({ title: '请输入持卡人姓名', icon: 'none' })
} else if (cleanCardNo(this.cardNoDisplay).length < 16) {
uni.showToast({ title: '请输入正确的银行卡号', icon: 'none' })
} else if (this.form.bank_name === '') {
uni.showToast({ title: '请选择开户银行', icon: 'none' })
} else if (!isValidPhone(this.form.phone)) {
uni.showToast({ title: '请输入正确的手机号码', icon: 'none' })
}
return
}
// Luhn 校验(可选拦截,不强制)
const rawCardNo = cleanCardNo(this.cardNoDisplay)
if (!luhnCheck(rawCardNo)) {
uni.showModal({
title: '卡号确认',
content: '银行卡号格式可能有误,是否继续?',
success: (res) => {
if (res.confirm) {
this.navigateToVerify()
}
}
})
return
}
this.navigateToVerify()
},
navigateToVerify() {
const rawCardNo = cleanCardNo(this.cardNoDisplay)
const params = encodeURIComponent(JSON.stringify({
holder_name: this.form.holder_name,
card_no: rawCardNo,
bank_name: this.form.bank_name,
bank_code: this.form.bank_code,
branch_name: this.form.branch_name,
phone: this.form.phone
}))
uni.navigateTo({
url: `/pages/mall/consumer/bank-cards/verify?data=${params}`
})
}
}
}
</script>
<style>
/* ── 整体结构 ────────────────────────────── */
.page {
flex: 1;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.scroll-wrap {
flex: 1;
}
/* ── 提示横幅 ─────────────────────────────── */
.tip-banner {
display: flex;
flex-direction: row;
align-items: center;
background-color: #fff7f0;
padding: 20rpx 32rpx;
margin-bottom: 20rpx;
}
.tip-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.tip-text {
font-size: 24rpx;
color: #ff6000;
}
/* ── 表单卡片 ────────────────────────────── */
.form-card {
background-color: #ffffff;
border-radius: 16rpx;
margin: 0 24rpx;
overflow: hidden;
}
.form-row {
display: flex;
flex-direction: row;
align-items: center;
min-height: 112rpx;
padding: 0 28rpx;
}
.selector-row {
/* 点击区域整行 */
}
.row-left {
display: flex;
flex-direction: row;
align-items: center;
width: 180rpx;
flex-shrink: 0;
}
.row-label {
font-size: 28rpx;
color: #222222;
font-weight: 500;
}
.optional-badge {
font-size: 20rpx;
color: #999999;
margin-left: 8rpx;
background-color: #f5f5f5;
padding: 2rpx 10rpx;
border-radius: 8rpx;
}
.row-right {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
}
.row-input {
flex: 1;
font-size: 28rpx;
color: #222222;
height: 80rpx;
line-height: 80rpx;
}
.row-tip-icon {
font-size: 36rpx;
color: #aaaaaa;
padding: 0 8rpx;
}
.camera-btn {
font-size: 42rpx;
padding: 0 4rpx;
color: #555555;
}
.selector-value-wrap {
flex: 1;
}
.selector-value {
font-size: 28rpx;
color: #222222;
}
.selector-value.placeholder {
color: #c0c4cc;
}
.arrow-icon {
font-size: 40rpx;
color: #cccccc;
padding-left: 8rpx;
}
.divider {
height: 1rpx;
background-color: #f2f2f2;
margin-left: 28rpx;
}
/* ── 安全说明 ────────────────────────────── */
.desc-area {
padding: 24rpx 32rpx 0;
}
.desc-text {
font-size: 22rpx;
color: #aaaaaa;
line-height: 1.7;
}
.bottom-placeholder {
height: 160rpx;
}
/* ── 底部按钮栏 ──────────────────────────── */
.bottom-bar {
background-color: #ffffff;
padding: 20rpx 32rpx 0;
border-top-width: 1rpx;
border-top-style: solid;
border-top-color: #f0f0f0;
}
.next-btn {
border-radius: 56rpx;
height: 96rpx;
display: flex;
align-items: center;
justify-content: center;
}
.next-btn-active {
background-color: #e23636;
}
.next-btn-disabled {
background-color: #f5a0a0;
}
.next-btn-text {
font-size: 32rpx;
color: #ffffff;
font-weight: 600;
}
/* ── 弹层背景蒙层 ────────────────────────── */
.picker-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 999;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
/* ── 银行选择面板 ────────────────────────── */
.picker-panel {
background-color: #ffffff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
max-height: 75%;
display: flex;
flex-direction: column;
}
.picker-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 32rpx 32rpx 20rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f0f0f0;
}
.picker-title {
font-size: 32rpx;
font-weight: bold;
color: #222222;
}
.picker-close {
font-size: 36rpx;
color: #999999;
padding: 8rpx;
}
.picker-scroll {
flex: 1;
}
.picker-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 28rpx 32rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f7f7f7;
}
.picker-item-active {
background-color: #fff5f5;
}
.bank-logo-placeholder {
width: 72rpx;
height: 72rpx;
border-radius: 36rpx;
background-color: #e23636;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
}
.bank-logo-text {
font-size: 22rpx;
color: #ffffff;
font-weight: bold;
}
.picker-item-name {
flex: 1;
font-size: 28rpx;
color: #333333;
}
.picker-check {
font-size: 32rpx;
color: #e23636;
font-weight: bold;
}
/* ── 支行输入面板 ────────────────────────── */
.branch-panel {
background-color: #ffffff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
padding-bottom: 40rpx;
}
.branch-input-wrap {
margin: 24rpx 32rpx;
background-color: #f7f7f7;
border-radius: 12rpx;
padding: 0 24rpx;
}
.branch-input {
height: 96rpx;
font-size: 28rpx;
color: #222222;
width: 100%;
}
.branch-confirm-btn {
margin: 0 32rpx;
height: 88rpx;
background-color: #e23636;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.branch-confirm-text {
font-size: 30rpx;
color: #ffffff;
font-weight: 600;
}
</style>