2026-01-05 12:44:57 +08:00

712 lines
18 KiB
Vue
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>
<div class="bank-card-container">
<!-- 页面标题 -->
<div class="header">
<h2>我的银行卡</h2>
<span class="subtitle">只能绑定一张银行卡</span>
</div>
<!-- 状态判断已绑定银行卡或未绑定 -->
<div v-if="hasBoundCard" class="bound-card-section">
<!-- 已绑定银行卡展示区域 -->
<div class="card-display">
<div class="card-header">
<div class="bank-icon">
<img src="https://gw.alipayobjects.com/zos/rmsportal/XuVpGqBFxXplzvLjJBZB.svg" alt="银联" />
</div>
<div class="card-number">{{ formatCardNumber(boundCard.cardNumber) }}</div>
</div>
<div class="card-details">
<div class="detail-item">
<span class="label">持卡人姓名</span>
<span class="value">{{ boundCard.realName }}</span>
</div>
<div class="detail-item">
<span class="label">开户行</span>
<span class="value">{{ boundCard.bankName }}</span>
</div>
<div class="detail-item">
<span class="label">手机号</span>
<span class="value">{{ boundCard.phone }}</span>
</div>
</div>
<div class="card-actions">
<a-button type="link" danger @click="showUnbindModal">解绑</a-button>
</div>
</div>
</div>
<!-- 未绑定银行卡时显示添加银行卡区域 -->
<div v-else class="add-card-section">
<div class="add-card-box" @click="showAddModal">
<div class="add-icon">
<plus-outlined />
</div>
<div class="add-text">添加银行卡</div>
</div>
</div>
<!-- 添加银行卡对话框 -->
<a-modal
v-model:visible="addModalVisible"
title="添加银行卡"
@ok="handleAddCard"
@cancel="handleAddCancel"
:confirm-loading="addLoading"
:width="500"
:ok-text="'绑定'"
:cancel-text="'取消'"
>
<a-form
ref="addFormRef"
:model="addFormState"
:rules="addFormRules"
layout="vertical"
>
<a-form-item label="真实姓名" name="realName">
<a-input
v-model:value="addFormState.realName"
placeholder="请输入持卡人真实姓名"
size="large"
/>
</a-form-item>
<a-form-item label="银行卡号" name="cardNumber">
<a-input
v-model:value="addFormState.cardNumber"
placeholder="请输入银行卡号"
size="large"
:maxlength="19"
@input="formatCardInput"
/>
</a-form-item>
<a-form-item label="开户行" name="bankName">
<a-select
v-model:value="addFormState.bankName"
placeholder="请选择开户行"
size="large"
:options="bankOptions"
show-search
/>
</a-form-item>
<a-form-item name="agreement">
<a-checkbox v-model:checked="addFormState.agreement">
绑定银行卡前我已阅读并确认
<a href="#" @click.prevent="showAgreement">银行卡绑定协议</a>
</a-checkbox>
</a-form-item>
</a-form>
</a-modal>
<!-- 解绑银行卡对话框 -->
<a-modal
v-model:visible="unbindModalVisible"
title="解绑银行卡"
@ok="handleUnbindCard"
@cancel="handleUnbindCancel"
:confirm-loading="unbindLoading"
:width="500"
:ok-text="'确认解绑'"
:cancel-text="'取消'"
>
<div class="unbind-info">
<div class="info-item">
<span class="label">持卡人姓名</span>
<span class="value">{{ boundCard.realName }}</span>
</div>
<div class="info-item">
<span class="label">银行卡号</span>
<span class="value">{{ formatCardNumber(boundCard.cardNumber) }}</span>
</div>
<div class="info-item">
<span class="label">开户行</span>
<span class="value">{{ boundCard.bankName }}</span>
</div>
<a-divider />
</div>
<a-form
ref="unbindFormRef"
:model="unbindFormState"
:rules="unbindFormRules"
layout="vertical"
>
<a-form-item label="手机号码" name="phone">
<a-input
v-model:value="unbindFormState.phone"
placeholder="请输入银行预留手机号"
size="large"
:maxlength="11"
/>
</a-form-item>
<a-form-item label="短信验证码" name="smsCode">
<div class="sms-code-input">
<a-input
v-model:value="unbindFormState.smsCode"
placeholder="请输入短信验证码"
size="large"
:maxlength="6"
/>
<a-button
type="primary"
:disabled="countdown > 0"
@click="sendSmsCode"
class="sms-button"
>
{{ countdown > 0 ? `${countdown}秒后重新获取` : '获取验证码' }}
</a-button>
</div>
</a-form-item>
<a-form-item name="agreement">
<a-checkbox v-model:checked="unbindFormState.agreement">
解绑银行卡前我已阅读并确认
<a href="#" @click.prevent="showUnbindAgreement">银行卡解绑协议</a>
</a-checkbox>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { message, Modal } from 'ant-design-vue'
// 已绑定银行卡数据 - 初始为空
const boundCard = ref(null)
// 是否已绑定银行卡 - 初始为false
const hasBoundCard = computed(() => {
return boundCard.value !== null && boundCard.value !== undefined
})
// 银行选项
const bankOptions = ref([
{ label: '中国工商银行', value: '中国工商银行' },
{ label: '中国建设银行', value: '中国建设银行' },
{ label: '中国农业银行', value: '中国农业银行' },
{ label: '中国银行', value: '中国银行' },
{ label: '交通银行', value: '交通银行' },
{ label: '招商银行', value: '招商银行' },
{ label: '中国邮政储蓄银行', value: '中国邮政储蓄银行' },
{ label: '中信银行', value: '中信银行' },
{ label: '光大银行', value: '光大银行' },
{ label: '华夏银行', value: '华夏银行' },
{ label: '民生银行', value: '民生银行' },
{ label: '兴业银行', value: '兴业银行' },
{ label: '浦发银行', value: '浦发银行' },
{ label: '平安银行', value: '平安银行' },
{ label: '广发银行', value: '广发银行' },
])
// 添加银行卡对话框状态
const addModalVisible = ref(false)
const addLoading = ref(false)
const addFormRef = ref()
const addFormState = reactive({
realName: '',
cardNumber: '',
bankName: '',
agreement: false
})
// 添加银行卡表单验证规则
const addFormRules = {
realName: [
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
{ pattern: /^[\u4e00-\u9fa5]{2,10}$/, message: '请输入2-10位中文字符', trigger: 'blur' }
],
cardNumber: [
{ required: true, message: '请输入银行卡号', trigger: 'blur' },
{
validator: (_, value) => {
const cleanValue = value.replace(/\s+/g, '')
return /^[0-9]{16,19}$/.test(cleanValue) ?
Promise.resolve() :
Promise.reject('银行卡号格式不正确16-19位数字')
},
trigger: 'blur'
}
],
bankName: [
{ required: true, message: '请选择开户行', trigger: 'change' }
],
agreement: [
{ validator: (_, value) => value ? Promise.resolve() : Promise.reject('请阅读并同意协议'), trigger: 'change' }
]
}
// 解绑银行卡对话框状态
const unbindModalVisible = ref(false)
const unbindLoading = ref(false)
const unbindFormRef = ref()
const unbindFormState = reactive({
phone: '',
smsCode: '',
agreement: false
})
// 短信验证码倒计时
const countdown = ref(0)
// 解绑银行卡表单验证规则
const unbindFormRules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
smsCode: [
{ required: true, message: '请输入短信验证码', trigger: 'blur' },
{ pattern: /^\d{6}$/, message: '验证码为6位数字', trigger: 'blur' }
],
agreement: [
{ validator: (_, value) => value ? Promise.resolve() : Promise.reject('请阅读并同意协议'), trigger: 'change' }
]
}
// 格式化银行卡号输入每4位加空格
const formatCardInput = (e) => {
let value = e.target.value.replace(/\s+/g, '').replace(/[^\d]/g, '')
let formattedValue = ''
for (let i = 0; i < value.length; i++) {
if (i > 0 && i % 4 === 0) {
formattedValue += ' '
}
formattedValue += value[i]
}
addFormState.cardNumber = formattedValue
}
// 显示添加银行卡对话框
const showAddModal = () => {
addModalVisible.value = true
// 重置表单
Object.assign(addFormState, {
realName: '',
cardNumber: '',
bankName: '',
agreement: false
})
}
// 显示解绑银行卡对话框
const showUnbindModal = () => {
unbindModalVisible.value = true
// 重置表单
Object.assign(unbindFormState, {
phone: '',
smsCode: '',
agreement: false
})
}
// 处理添加银行卡
const handleAddCard = () => {
addFormRef.value.validate().then(() => {
addLoading.value = true
// 模拟API请求
setTimeout(() => {
// 清除空格,保存纯数字
const cleanCardNumber = addFormState.cardNumber.replace(/\s+/g, '')
// 更新已绑定银行卡信息
boundCard.value = {
realName: addFormState.realName,
cardNumber: cleanCardNumber,
bankName: addFormState.bankName,
phone: '138****' + cleanCardNumber.substr(cleanCardNumber.length - 4) // 模拟手机号
}
addModalVisible.value = false
addLoading.value = false
message.success('银行卡绑定成功')
}, 1000)
}).catch(error => {
console.log('验证失败:', error)
})
}
// 处理解绑银行卡
const handleUnbindCard = () => {
unbindFormRef.value.validate().then(() => {
unbindLoading.value = true
// 模拟API请求
setTimeout(() => {
// 清除已绑定银行卡信息
boundCard.value = null
unbindModalVisible.value = false
unbindLoading.value = false
message.success('银行卡解绑成功')
}, 1000)
}).catch(error => {
console.log('验证失败:', error)
})
}
// 发送短信验证码
const sendSmsCode = () => {
if (!unbindFormState.phone) {
message.warning('请输入手机号码')
return
}
if (!/^1[3-9]\d{9}$/.test(unbindFormState.phone)) {
message.warning('手机号格式不正确')
return
}
// 模拟验证手机号是否匹配
const expectedPhone = boundCard.value.phone.replace(/\*/g, '')
const inputPhone = unbindFormState.phone
const lastFourDigits = boundCard.value.cardNumber.substr(boundCard.value.cardNumber.length - 4)
if (inputPhone !== '138' + lastFourDigits) {
message.error('手机号与预留手机号不匹配')
return
}
// 开始倒计时
countdown.value = 60
const timer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
// 模拟发送验证码实际为123456
message.success('验证码已发送请注意查收演示验证码123456')
unbindFormState.smsCode = '123456'
}
// 格式化银行卡号显示
const formatCardNumber = (cardNumber) => {
if (!cardNumber) return ''
const cleanNumber = cardNumber.replace(/\s+/g, '')
const visibleDigits = 4
const maskedPart = '*'.repeat(cleanNumber.length - visibleDigits * 2)
const part1 = cleanNumber.substring(0, visibleDigits)
const part2 = cleanNumber.substring(cleanNumber.length - visibleDigits)
return `${part1} ${maskedPart} ${part2}`
}
// 取消添加银行卡
const handleAddCancel = () => {
addModalVisible.value = false
}
// 取消解绑银行卡
const handleUnbindCancel = () => {
unbindModalVisible.value = false
}
// 显示协议(示例)
const showAgreement = () => {
Modal.info({
title: '银行卡绑定协议',
content: `
<div style="max-height: 400px; overflow-y: auto;">
<h4>一、总则</h4>
<p>1. 本协议旨在明确用户绑定银行卡的相关权利和义务。</p>
<p>2. 用户绑定银行卡前,应仔细阅读并理解本协议的全部内容。</p>
<h4>二、用户义务</h4>
<p>1. 用户应确保绑定的银行卡为本人名下有效的银行卡。</p>
<p>2. 用户应妥善保管银行卡信息,不得泄露给他人。</p>
<p>3. 用户应确保提供的所有信息真实、准确、完整。</p>
<h4>三、平台责任</h4>
<p>1. 平台应保障用户银行卡信息的安全。</p>
<p>2. 平台不得未经用户同意擅自使用银行卡信息。</p>
<h4>四、风险提示</h4>
<p>1. 用户应自行承担因银行卡信息泄露造成的损失。</p>
<p>2. 如发现异常情况,应立即联系平台客服。</p>
</div>
`,
width: 600,
okText: '我已阅读并同意'
})
}
// 显示解绑协议(示例)
const showUnbindAgreement = () => {
Modal.info({
title: '银行卡解绑协议',
content: `
<div style="max-height: 400px; overflow-y: auto;">
<h4>一、解绑须知</h4>
<p>1. 解绑银行卡将解除该银行卡与账户的绑定关系。</p>
<p>2. 解绑后,该银行卡将无法用于账户的相关操作。</p>
<h4>二、解绑条件</h4>
<p>1. 账户内无正在进行中的交易。</p>
<p>2. 账户余额为零(如有余额需先提现)。</p>
<p>3. 无未完成的订单或服务。</p>
<h4>三、解绑流程</h4>
<p>1. 验证持卡人身份信息。</p>
<p>2. 输入正确的短信验证码。</p>
<p>3. 确认解绑操作。</p>
<h4>四、注意事项</h4>
<p>1. 解绑操作不可撤销,请谨慎操作。</p>
<p>2. 解绑后如需重新绑定,需重新验证身份。</p>
<p>3. 如遇问题,请联系客服协助处理。</p>
</div>
`,
width: 600,
okText: '我已阅读并理解'
})
}
</script>
<style scoped>
.bank-card-container {
max-width: 100%;
margin: 0 auto;
padding: 32px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
min-height: 400px;
}
.header {
display: flex;
align-items: center;
margin-bottom: 40px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.header h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.header .subtitle {
margin-left: 12px;
color: #1890ff;
font-size: 14px;
padding-top: 4px;
}
.add-card-section, .bound-card-section {
/* display: flex;
justify-content: center; */
align-items: center;
min-height: 320px;
}
.add-card-box {
width: 320px;
height: 200px;
border: 2px dashed #d9d9d9;
border-radius: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fafafa;
}
.add-card-box:hover {
border-color: #1890ff;
border-style: solid;
background-color: #f0f8ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.1);
}
.add-icon {
font-size: 56px;
color: #bfbfbf;
margin-bottom: 20px;
transition: all 0.3s;
}
.add-card-box:hover .add-icon {
color: #1890ff;
}
.add-text {
font-size: 20px;
color: #8c8c8c;
font-weight: 500;
transition: all 0.3s;
}
.add-card-box:hover .add-text {
color: #1890ff;
}
.card-display {
width: 450px;
height: 240px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 28px;
color: white;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
}
.card-display::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%);
border-radius: 50%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 36px;
position: relative;
z-index: 1;
}
.bank-icon img {
width: 64px;
height: 64px;
background-color: white;
border-radius: 10px;
padding: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-number {
font-size: 26px;
font-weight: 600;
letter-spacing: 3px;
font-family: 'Courier New', monospace;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.card-details {
margin-bottom: 20px;
position: relative;
z-index: 1;
}
.detail-item {
margin-bottom: 14px;
font-size: 16px;
display: flex;
}
.detail-item .label {
opacity: 0.85;
width: 100px;
}
.detail-item .value {
font-weight: 500;
flex: 1;
}
.card-actions {
position: absolute;
bottom: 24px;
right: 28px;
z-index: 1;
}
:deep(.ant-btn-link) {
color: rgba(255, 255, 255, 0.9) !important;
font-size: 16px;
padding: 4px 8px;
border-radius: 4px;
}
:deep(.ant-btn-link:hover) {
color: white !important;
background-color: rgba(255, 255, 255, 0.15);
}
.unbind-info {
background-color: #f6f8fa;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.info-item {
margin-bottom: 12px;
font-size: 15px;
display: flex;
}
.info-item .label {
color: #666;
width: 100px;
}
.info-item .value {
color: #333;
font-weight: 500;
flex: 1;
}
.sms-code-input {
display: flex;
gap: 12px;
}
.sms-code-input :deep(.ant-input) {
flex: 1;
}
.sms-button {
white-space: nowrap;
}
:deep(.ant-modal-body) {
padding: 24px;
}
:deep(.ant-form-item) {
margin-bottom: 20px;
}
:deep(.ant-input-lg) {
border-radius: 6px;
}
:deep(.ant-select-lg) {
border-radius: 6px;
}
:deep(.ant-checkbox-wrapper) {
font-size: 14px;
}
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
background-color: #1890ff;
border-color: #1890ff;
}
</style>