银行卡管理

This commit is contained in:
qiuyuan 2026-01-05 12:44:57 +08:00
parent 336e9b4e42
commit 82b1ca53c7
3 changed files with 719 additions and 0 deletions

View File

@ -232,6 +232,12 @@ const routes: RouteRecordRaw[] = [
component: () => component: () =>
import("@/views/admin/account/msgCenter/index.vue"), import("@/views/admin/account/msgCenter/index.vue"),
}, },
{
path: "bankCard",
name: "bankCard",
component: () =>
import("@/views/admin/account/bankCard/index.vue"),
},
{ {
path: "image", path: "image",
name: "Image", name: "Image",

View File

@ -0,0 +1,712 @@
<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>

View File

@ -88,6 +88,7 @@ const menuItems: MenuItem[] = [
{ path: '/layout/admin/myCertificate', name: '我的算力券', visible: true }, { path: '/layout/admin/myCertificate', name: '我的算力券', visible: true },
{ path: '/layout/admin/myInvite', name: '我的邀请', visible: true }, { path: '/layout/admin/myInvite', name: '我的邀请', visible: true },
{ path: '/layout/admin/msgCenter', name: '消息中心', visible: true }, { path: '/layout/admin/msgCenter', name: '消息中心', visible: true },
{ path: '/layout/admin/bankCard', name: '银行卡管理', visible: true },
], ],
}, },
]; ];