This commit is contained in:
2026-05-11 15:52:54 +08:00
parent a4978f8ed3
commit d1a1cd1976
7 changed files with 435 additions and 227 deletions

View File

@ -16,3 +16,13 @@ export const editChannel = (params: any) => {
export const delChannel = (params: any) => {
return request.post('/channel/delChannel', { ...params })
}
// 获取用户充值渠道列表
export const getRechargeChannelList = () => {
return request.get('/user/regchargeChannel')
}
// 用户充值
export const rechargeApi = (params: any) => {
return request.post('/user/recharge', { ...params })
}

View File

@ -3,7 +3,7 @@
<!-- 页面标题 -->
<div class="page-title">存款</div>
<!-- 存款渠道列表每行提供一个存款按钮 -->
<!-- 存款渠道列表每行提供一个"存款"按钮 -->
<RechangeTable :table-data="channelList" :columns="tableColumns" :highlight-current-row="false">
<template #action="{ row }">
<el-button type="primary" size="small" @click="handleOpenDialog(row)">
@ -13,218 +13,318 @@
</RechangeTable>
</div>
<!-- 存款弹窗改为和设计图一致的支付面板布局 -->
<RechangeDialog class="rechange-pay-dialog" :visible="dialogVisible" title=" " width="640px" :show-footer="false"
<!-- 存款弹窗 -->
<RechangeDialog class="rechange-pay-dialog" :visible="dialogVisible" title="存款" width="600px" :show-footer="false"
@cancel="handleCancel" @close="handleClose">
<div class="dialog-content">
<!-- 顶部标题展示当前点击的存款渠道名称 -->
<div class="dialog-page-title">{{ rechangeForm.channelName || '存款渠道名称' }}</div>
<!-- 存款面板当前为纯前端演示后续可在确认支付时接入真实接口 -->
<el-form ref="rechangeFormRef" :model="rechangeForm" :rules="rechangeFormRules" label-position="top"
class="pay-form">
<el-form-item prop="depositAccount">
<template #label>
<span class="required-label">存款账户</span>
</template>
<el-input v-model="rechangeForm.depositAccount" disabled />
</el-form-item>
<el-form-item prop="supportedCurrency">
<template #label>
<span class="required-label">支持币种</span>
</template>
<div class="currency-box">{{ rechangeForm.supportedCurrency }}</div>
</el-form-item>
<el-form-item prop="depositAmount">
<template #label>
<span class="required-label">存款金额</span>
</template>
<el-input v-model="rechangeForm.depositAmount" placeholder="请输入金额">
<template #prepend>{{ rechangeForm.amountCurrency }}</template>
</el-input>
<!-- 快捷金额模拟截图中的金额快捷按钮 -->
<div class="quick-amounts">
<el-button v-for="amount in rechangeQuickAmounts" :key="amount" plain size="small"
@click="handleQuickAmount(amount)">
{{ amount }}
</el-button>
<el-form ref="rechangeFormRef" :model="rechangeForm" :rules="rechangeFormRules" label-position="top">
<!-- 渠道信息 -->
<div class="channel-card">
<div class="channel-info">
<div class="channel-name">{{ rechangeForm.channelName }}</div>
<div class="channel-meta">
<span></span>
</div>
</div>
<div class="channel-meta">支持货币: {{ rechangeForm.currencySymbol }}</div>
</div>
<!-- 银行卡信息channel_code = BANK 时显示 -->
<template v-if="rechangeForm.channelCode === 'BANK'">
<div class="info-section">
<div class="section-title">收款信息</div>
<div class="info-grid">
<div class="info-item">
<span class="label">账户持有人姓名</span>
<span class="value">{{ rechangeForm.beneficiaryName }}</span>
</div>
<div class="info-item">
<span class="label">银行名称</span>
<span class="value">{{ rechangeForm.beneficiaryBankName }}</span>
</div>
<div class="info-item">
<span class="label">银行识别代码</span>
<span class="value">{{ rechangeForm.swiftBicCode }}</span>
</div>
<div class="info-item">
<span class="label">收款人银行账号</span>
<span class="value">{{ rechangeForm.beneficiaryBankAccount }}</span>
</div>
<div class="info-item full-width">
<span class="label">银行地址</span>
<span class="value">{{ rechangeForm.beneficiaryBankAddress }}</span>
</div>
</div>
</div>
</template>
<!-- 钱包地址信息其他渠道显示 -->
<template v-else>
<div class="info-section">
<div class="section-title">收款信息</div>
<div class="info-item full-width">
<span class="label">钱包收款地址</span>
<span class="value address-value">{{ rechangeForm.address }}</span>
</div>
</div>
</template>
<!-- 充值金额 -->
<el-form-item label="充值金额" prop="depositAmount">
<el-input v-model="rechangeForm.depositAmount" placeholder="请输入充值金额" size="large" @input="handleAmountInput">
<template #suffix>
<span class="currency-suffix">{{ rechangeForm.currencySymbol }}</span>
</template>
</el-input>
</el-form-item>
<!-- 上传凭证 -->
<el-form-item label="上传凭证" prop="voucherUrl">
<el-upload class="voucher-uploader" action="" :show-file-list="false"
:http-request="handleUpload" :before-upload="beforeAvatarUpload" :loading="uploadLoading">
<img v-if="voucherUrl" :src="voucherUrl" class="voucher-image" />
<div v-else class="upload-placeholder">
<el-icon class="upload-icon">
<Plus />
</el-icon>
<span class="upload-text">点击上传凭证</span>
</div>
</el-upload>
</el-form-item>
</el-form>
<!-- 按钮区上一步用于关闭弹窗返回列表确认支付用于提交当前输入 -->
<!-- 按钮区 -->
<div class="dialog-actions">
<el-button class="prev-btn" @click="handlePrevStep">上一步</el-button>
<el-button type="primary" class="confirm-btn" @click="handleSubmit">确认支付</el-button>
</div>
<el-button size="large" @click="handleCancel">取消</el-button>
<el-button type="primary" size="large" :loading="submitLoading" @click="handleSubmit">
确认充值
</el-button>
</div>
</RechangeDialog>
</template>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
defineOptions({
name: 'Rechange'
})
import type { FormInstance, FormRules } from 'element-plus'
import type { FormInstance, FormRules, UploadProps } from 'element-plus'
import { ElMessage } from 'element-plus'
import { nextTick, ref } from 'vue'
import { nextTick, onMounted, ref } from 'vue'
import RechangeTable from '@/components/common/Table.vue'
import RechangeDialog from '@/components/common/Dialog.vue'
import { rechangeQuickAmounts, rechangeTableColumns } from './options'
import { rechangeTableColumns } from './options'
import { uploadImageApi } from '@/api/modules/upload'
import { getRechargeChannelList, rechargeApi } from '@/api/modules/capital/rechangeChanl'
//
interface RechangeRow {
//
interface ChannelCurrency {
id: number
depositChannel: string
channelName: string
supportedCurrency: string
processingTime: string
singleAmount: string
depositAccount: string
amountCurrency: string
symbol: string
rate: string
}
//
interface RechangeFormData {
interface ChannelRow {
id: number
channelName: string
depositAccount: string
supportedCurrency: string
amountCurrency: string
depositAmount: string
name: string
channel_fee: string
type: number
beneficiary_name: string | null
beneficiary_bank_name: string | null
swift_bic_code: string | null
beneficiary_bank_address: string | null
beneficiary_bank_account: string | null
channel_code: string
currency: ChannelCurrency
address?: string
}
// mock
const channelList = ref<RechangeRow[]>([
{
id: 1,
depositChannel: 'BANK-CNY',
channelName: '存款渠道名称',
supportedCurrency: 'CNY',
processingTime: '5-10分钟',
singleAmount: '100 - 50,000',
depositAccount: '只能在总钱包充值',
amountCurrency: 'USD',
},
{
id: 2,
depositChannel: 'BANK-USD',
channelName: '银行卡入金',
supportedCurrency: 'USD',
processingTime: '5-10分钟',
singleAmount: '100 - 50,000',
depositAccount: '只能在总钱包充值',
amountCurrency: 'USD',
},
{
id: 3,
depositChannel: 'USDT-TRC20',
channelName: '数字货币入金',
supportedCurrency: 'USDT',
processingTime: '1-3分钟',
singleAmount: '50 - 100,000',
depositAccount: '只能在总钱包充值',
amountCurrency: 'USDT',
},
])
//
const tableColumns = ref(rechangeTableColumns)
//
const dialogVisible = ref(false)
const currentRowId = ref<number | null>(null)
const rechangeFormRef = ref<FormInstance>()
const submitLoading = ref(false)
//
const voucherUrl = ref('')
const uploadLoading = ref(false)
//
const channelList = ref<ChannelRow[]>([])
//
interface RechangeFormData {
id: number
channelName: string
channelCode: string
currencySymbol: string
channelFee: string
depositAmount: string
voucherUrl: string
beneficiaryName: string
beneficiaryBankName: string
swiftBicCode: string
beneficiaryBankAddress: string
beneficiaryBankAccount: string
address: string
}
//
const createDefaultForm = (): RechangeFormData => ({
id: 0,
channelName: '',
depositAccount: '',
supportedCurrency: '',
amountCurrency: '',
channelCode: '',
currencySymbol: '',
channelFee: '0',
depositAmount: '',
voucherUrl: '',
beneficiaryName: '',
beneficiaryBankName: '',
swiftBicCode: '',
beneficiaryBankAddress: '',
beneficiaryBankAccount: '',
address: '',
})
const rechangeForm = ref<RechangeFormData>(createDefaultForm())
//
//
const rechangeFormRules = ref<FormRules<RechangeFormData>>({
depositAmount: [
{ required: true, message: '请输入存款金额', trigger: ['blur', 'change'] },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '金额格式不正确最多保留2位小数', trigger: ['blur', 'change'] },
{ required: true, message: '请输入充值金额', trigger: ['blur', 'change'] },
{ pattern: /^\d+$/, message: '请输入整数金额', trigger: ['blur', 'change'] },
],
voucherUrl: [
{ required: true, message: '请上传充值凭证', trigger: ['change'] },
],
})
//
const resetDialogForm = () => {
rechangeForm.value = createDefaultForm()
currentRowId.value = null
rechangeFormRef.value?.clearValidate()
//
const fetchChannelList = async () => {
try {
const res = await getRechargeChannelList() as ChannelRow[]
channelList.value = res || []
} catch (error) {
console.error('获取充值渠道列表失败', error)
}
}
//
const handleOpenDialog = async (row: RechangeRow) => {
currentRowId.value = row.id
//
const handleOpenDialog = async (row: ChannelRow) => {
rechangeForm.value = {
id: row.id,
channelName: row.channelName,
depositAccount: row.depositAccount,
supportedCurrency: row.supportedCurrency,
amountCurrency: row.amountCurrency,
channelName: row.name,
channelCode: row.channel_code,
currencySymbol: row.currency.symbol,
channelFee: row.channel_fee || '0',
depositAmount: '',
voucherUrl: '',
beneficiaryName: row.beneficiary_name || '',
beneficiaryBankName: row.beneficiary_bank_name || '',
swiftBicCode: row.swift_bic_code || '',
beneficiaryBankAddress: row.beneficiary_bank_address || '',
beneficiaryBankAccount: row.beneficiary_bank_account || '',
address: (row as any).address || '',
}
voucherUrl.value = ''
dialogVisible.value = true
await nextTick()
rechangeFormRef.value?.clearValidate()
}
//
const handleQuickAmount = (amount: string) => {
rechangeForm.value.depositAmount = amount
//
const handleAvatarSuccess: UploadProps['onSuccess'] = (response: any) => {
if (response.code === 0) {
voucherUrl.value = response.url
rechangeForm.value.voucherUrl = response.url
ElMessage.success('上传成功')
} else {
ElMessage.error(response.msg || '上传失败')
}
uploadLoading.value = false
}
//
const handlePrevStep = () => {
dialogVisible.value = false
resetDialogForm()
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
ElMessage.error('图片格式必须为 JPG 或 PNG!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
uploadLoading.value = true
return true
}
//
//
const handleUpload = async (options: any) => {
const { file, onSuccess, onError } = options
try {
const res: any = await uploadImageApi(file)
voucherUrl.value = res.url
rechangeForm.value.voucherUrl = res.url
onSuccess(res)
//
nextTick(() => {
rechangeFormRef.value?.clearValidate('voucherUrl')
})
} catch (error) {
onError(error)
uploadLoading.value = false
}
}
//
const handleAmountInput = (value: string) => {
const filtered = value.replace(/\D/g, '')
rechangeForm.value.depositAmount = filtered
}
//
const handleSubmit = async () => {
try {
await rechangeFormRef.value?.validate()
ElMessage.success(`已进入支付确认流程渠道ID${currentRowId.value}`)
submitLoading.value = true
await rechargeApi({
channel_id: rechangeForm.value.id,
amount: rechangeForm.value.depositAmount,
img: rechangeForm.value.voucherUrl,
})
ElMessage.success('充值提交成功')
dialogVisible.value = false
resetDialogForm()
} catch (error) {
console.error('存款表单校验失败', error)
fetchChannelList()
} catch (error: any) {
ElMessage.error(error?.msg || '充值提交失败')
} finally {
submitLoading.value = false
}
}
//
const handleCancel = () => {
dialogVisible.value = false
resetDialogForm()
rechangeFormRef.value?.resetFields()
rechangeForm.value = createDefaultForm()
}
//
const handleClose = () => {
dialogVisible.value = false
resetDialogForm()
rechangeFormRef.value?.resetFields()
rechangeForm.value = createDefaultForm()
}
//
onMounted(() => {
fetchChannelList()
})
</script>
<style scoped lang="scss">
.rechange-page {
height: 100%;
//
.page-title {
margin-bottom: 16px;
font-size: 28px;
@ -233,99 +333,172 @@ const handleClose = () => {
}
}
.dialog-content {
padding: 6px 8px 0;
.channel-card {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
color: #fff;
.channel-info {
.channel-name {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.channel-meta {
display: flex;
gap: 16px;
font-size: 14px;
opacity: 0.9;
}
}
.channel-fee {
font-size: 16px;
font-weight: 600;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.2);
border-radius: 20px;
}
}
.dialog-page-title {
margin-bottom: 24px;
font-size: 24px;
.info-section {
background: #f5f7fa;
border-radius: 10px;
padding: 16px;
margin-bottom: 20px;
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.pay-form {
:deep(.el-form-item) {
margin-bottom: 28px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
:deep(.el-form-item__label) {
padding-bottom: 10px;
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
.info-item {
display: flex;
flex-direction: column;
gap: 6px;
&.full-width {
grid-column: span 2;
}
:deep(.el-input__wrapper),
:deep(.el-input-group__prepend) {
height: 46px;
.label {
font-size: 13px;
color: #909399;
}
.value {
font-size: 14px;
}
}
.required-label {
position: relative;
padding-left: 12px;
font-size: 16px;
color: #303133;
word-break: break-all;
&::before {
position: absolute;
left: 0;
top: 1px;
color: #f56c6c;
content: '*';
&.address-value {
font-family: monospace;
background: #fff;
padding: 8px;
border-radius: 4px;
}
}
}
}
}
.currency-box {
display: inline-flex;
.currency-suffix {
font-size: 16px;
font-weight: 500;
color: #606266;
}
//
.voucher-uploader {
width: 100%;
height: 160px;
border: 1px dashed var(--el-border-color);
border-radius: 8px;
cursor: pointer;
overflow: hidden;
transition: var(--el-transition-duration-fast);
&:hover {
border-color: var(--el-color-primary);
}
:deep(.el-upload) {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
min-width: 116px;
height: 60px;
padding: 0 24px;
font-size: 18px;
color: #303133;
background: #fff;
border: 1px solid #dcdfe6;
box-sizing: border-box;
}
}
.quick-amounts {
.voucher-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.upload-placeholder {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: #909399;
:deep(.el-button) {
min-width: 72px;
margin-left: 0;
color: #3b82f6;
border-color: #3b82f6;
.upload-icon {
font-size: 32px;
}
.upload-text {
font-size: 14px;
}
}
}
//
.dialog-actions {
display: flex;
gap: 44px;
margin-top: 44px;
padding-left: 6px;
}
gap: 12px;
margin-top: 24px;
.prev-btn,
.confirm-btn {
min-width: 98px;
.el-button {
flex: 1;
height: 44px;
font-size: 16px;
border-radius: 8px;
}
}
.prev-btn {
color: #3b82f6;
border-color: #3b82f6;
}
// Element Plus Dialog
:deep(.rechange-pay-dialog .el-dialog__header) {
display: none;
padding: 20px 24px;
border-bottom: 1px solid #e4e7ed;
}
:deep(.rechange-pay-dialog .el-dialog__body) {
padding: 26px 26px 30px;
padding: 24px;
}
:deep(.rechange-pay-dialog .el-dialog) {
border-radius: 12px;
overflow: hidden;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
</style>

View File

@ -1,12 +1,11 @@
// 存款列表表格列配置:当前页面仍然保留列表 + 行内操作按钮的结构。
// 存款列表表格列配置
export const rechangeTableColumns = [
{ prop: 'depositChannel', label: '存款渠道', align: 'center' as const },
{ prop: 'channelName', label: '渠道名称', align: 'center' as const },
{ prop: 'supportedCurrency', label: '支持货币', align: 'center' as const },
{ prop: 'processingTime', label: '预计处理时间', align: 'center' as const },
{ prop: 'singleAmount', label: '单笔存款金额', align: 'center' as const },
{ prop: 'name', label: '渠道名称', align: 'center' as const },
{
prop: 'currency.symbol',
label: '支持货币',
align: 'center' as const
},
{ prop: 'channel_fee', label: '渠道费用', align: 'center' as const },
{ label: '操作', slotName: 'action', align: 'center' as const, width: 120 },
]
// 存款弹窗快捷金额:点击后直接回填到金额输入框。
export const rechangeQuickAmounts = ['100.00', '500.00', '1000.00', '2000.00', '5000.00']

View File

@ -92,7 +92,9 @@ const formData = ref({
beneficiary_name: '',
beneficiary_bank_name: '',
swift_bic_code: '',
beneficiary_bank_account: '',
beneficiary_bank_address: '',
address: '',
channel_code: '',
status: 1,
})
@ -128,7 +130,9 @@ const resetFormData = () => {
beneficiary_name: '',
beneficiary_bank_name: '',
swift_bic_code: '',
bank_account: '',
beneficiary_bank_address: '',
address: '',
channel_code: '',
}
}
@ -195,3 +199,4 @@ onMounted(() => {
}
}
</style>

View File

@ -52,12 +52,24 @@ export const formItemsData = [
type: 'input',
placeholder: '请输入银行识别代码',
},
{
label: '银行账号',
prop: 'beneficiary_bank_account',
type: 'input',
placeholder: '请输入银行账号',
},
{
label: '银行地址',
prop: 'beneficiary_bank_address',
type: 'input',
placeholder: '请输入银行地址',
},
{
label: 'U钱包收款地址',
prop: 'address',
type: 'input',
placeholder: '请输入U钱包收款地址',
},
{ label: '渠道编码', prop: 'channel_code', type: 'input', placeholder: '请输入渠道编码' },
{ label: '状态', prop: 'status', type: 'radio' },
]
@ -73,7 +85,9 @@ export const formRulesData = {
beneficiary_name: [{ required: true, message: '请输入账户持有人姓名', trigger: ['change'] }],
beneficiary_bank_name: [{ required: true, message: '请输入银行名称', trigger: ['change'] }],
swift_bic_code: [{ required: true, message: '请输入银行识别代码', trigger: ['change'] }],
beneficiary_bank_account: [{ required: true, message: '请输入银行账号', trigger: ['change'] }],
beneficiary_bank_address: [{ required: true, message: '请输入银行地址', trigger: ['change'] }],
address: [{ required: true, message: '请输入U钱包收款地址', trigger: ['change'] }],
channel_code: [{ required: true, message: '请输入渠道编码', trigger: ['change'] }],
}

View File

@ -190,7 +190,14 @@ const marketForm = ref({
})
const marketFormRules = rules(marketForm.value)
const marketFormRef = ref()
const marketFormItemOptions = ref(marketFormItem)
const marketFormItemOptions = computed(() => {
return marketFormItem.map((item) => {
if (item.prop === 'parentAccountName' || item.prop === 'level') {
return { ...item, disabled: dialogType.value === 'edit' }
}
return item
})
})
interface SelectOption {
label: string
value: string | number

View File

@ -58,7 +58,7 @@ export const marketFormItem = [
//级别
{ label: '级别', prop: 'level', type: 'select' as const, placeholder: '请选择级别' },
//所属上级
{ label: '所属上级', prop: 'parentAccountName', type: 'select' as const, placeholder: '请选择所属上级账户名称', },
{ label: '所属上级', prop: 'parentAccountName', type: 'select' as const, placeholder: '请选择所属上级账户名称' },
// 风控模板
{ label: '风控模板', prop: 'riskControlTemplate', type: 'select' as const, placeholder: '请选择风控模板' },
//头寸