feat(新页面、对接接口): 新增提交银行卡信息页,完成提现相关的接口对接

This commit is contained in:
hzj 2025-11-11 17:36:47 +08:00
parent a1afeba9df
commit a184ddfae3
10 changed files with 1106 additions and 116 deletions

View File

@ -60,17 +60,6 @@ export const cancelInvite = async (userId) => {
}
}
// 提交提现申请
export const withdrawApply = async (data) => {
try {
const response = await post('/team/bd/withdraw/apply', data)
return response
} catch (error) {
console.error('Failed to fetch withdraw apply:', error)
throw error
}
}
// 查询BD/BDLearer 成员账单列表
export const getBdMemberBillList = async (type) => {
try {

View File

@ -323,3 +323,14 @@ export const updateWithdrawInfo = async (data) => {
throw error
}
}
// 提交提现申请
export const withdrawApply = async (data) => {
try {
const response = await post('/wallet/bank/withdraw', data)
return response
} catch (error) {
console.error('Failed to fetch withdraw apply:', error)
throw error
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

View File

@ -317,6 +317,12 @@ const router = createRouter({
component: () => import('../views/Activities/heroesDay/index.vue'),
meta: { requiresAuth: true },
},
{
path: '/bank-card',
name: 'bank-card',
component: () => import('../views/Wallet/CashOut/BankCard.vue'),
meta: { requiresAuth: true },
},
],
})

View File

@ -8,39 +8,47 @@ export const PageConfig = {
needsBankBalance: true,
needsWorkStatistics: true,
needsRouteGuard: true,
title: "Host Center",
userTag: "👑 Host",
title: 'Host Center',
userTag: '👑 Host',
},
COIN_SELLER: {
needsBankBalance: true,
needsWorkStatistics: false, // Coin Seller 不需要工作统计
needsRouteGuard: true,
title: "Coin Seller",
userTag: "💰 Coin Seller",
title: 'Coin Seller',
userTag: '💰 Coin Seller',
},
AGENCY_CENTER: {
needsBankBalance: true,
needsWorkStatistics: true,
needsRouteGuard: true,
title: "Agency Center",
userTag: "🏢 Agency",
title: 'Agency Center',
userTag: '🏢 Agency',
},
BD_LEADER_CENTER: {
needsBankBalance: true,
needsWorkStatistics: false, // BD 可能不需要日常工作统计
needsRouteGuard: true,
title: 'BD Leader Center',
userTag: '📈 BD Leader',
},
BD_CENTER: {
needsBankBalance: true,
needsWorkStatistics: false, // BD 可能不需要日常工作统计
needsRouteGuard: true,
title: "BD Center",
userTag: "📈 BD",
title: 'BD Center',
userTag: '📈 BD',
},
APPLY: {
needsBankBalance: false, // 申请页面不需要余额
needsWorkStatistics: false, // 申请页面不需要工作统计
needsRouteGuard: false, // 申请页面不需要身份检查
title: "Apply",
title: 'Apply',
userTag: null, // 申请页面可能没有身份标签
},
@ -48,10 +56,10 @@ export const PageConfig = {
needsBankBalance: false,
needsWorkStatistics: false, // ADMIN 可能不需要日常工作统计
needsRouteGuard: true,
title: "ADMIN Center",
userTag: "ADMIN",
title: 'ADMIN Center',
userTag: 'ADMIN',
},
};
}
/**
* 获取页面配置的工具函数
@ -59,16 +67,16 @@ export const PageConfig = {
* @returns {Object} 页面配置对象
*/
export function getPageConfig(pageType) {
const config = PageConfig[pageType];
const config = PageConfig[pageType]
if (!config) {
console.warn(`未找到页面配置: ${pageType}`);
return PageConfig.HOST_CENTER; // 默认使用 HOST_CENTER 配置
console.warn(`未找到页面配置: ${pageType}`)
return PageConfig.HOST_CENTER // 默认使用 HOST_CENTER 配置
}
return config;
return config
}
// ==========================================
import { usePageInitialization } from "./usePageInitialization.js";
import { usePageInitialization } from './usePageInitialization.js'
/**
* 使用预配置的页面初始化函数
@ -77,13 +85,13 @@ import { usePageInitialization } from "./usePageInitialization.js";
* @returns {Object} usePageInitialization 的返回值
*/
export function usePageInitializationWithConfig(pageType, customOptions = {}) {
const pageConfig = getPageConfig(pageType);
const pageConfig = getPageConfig(pageType)
// 合并默认配置和自定义选项
const options = {
...pageConfig,
...customOptions,
};
}
return usePageInitialization(options);
return usePageInitialization(options)
}

View File

@ -143,6 +143,7 @@ export const ROLE_PERMISSIONS = {
'/cash-out', // 提现页面
'/cash-out-details', //提现详情页面
'/KYC', //提现资料提交
'/bank-card', //选择银行卡
],
// 加载页面

View File

@ -422,6 +422,7 @@ class RouteGuard {
'/cash-out', // 提现页面
'/cash-out-details', //提现详情页面
'/KYC', //提现资料提交
'/bank-card', //选择银行卡
]
return publicPages.includes(path)
}

View File

@ -40,7 +40,7 @@
</div>
</div>
<!-- 操作按钮 -->
<!-- 切换按钮 -->
<div
style="
display: flex;
@ -126,7 +126,7 @@
cursor: pointer;
transition: all 0.2s;
"
@click="handleExchange"
@click="exchangeTipShow = true"
:disabled="!selectedExchangeOption"
:style="{
background: !selectedExchangeOption
@ -320,14 +320,61 @@
<!-- Cash out -->
<div v-if="activeAction === 'Cash out'">
<!-- 提现资料 -->
<!-- 银行帐号信息 -->
<!-- KYC验证资料 -->
<div>
<!-- 标题 -->
<div style="display: flex; align-items: center; margin-bottom: 10px">
<div style="display: flex; align-items: flex-end; margin-bottom: 10px">
<div style="font-weight: 600">{{ t('kyc') }}</div>
<!-- 申请状态 -->
<div v-if="hasKYC" style="font-weight: 600; margin: 0 4px">:</div>
<!-- 通过 -->
<div
v-if="bankCardInfo?.status === 'PASS'"
style="
font-weight: 510;
font-size: 0.9em;
background: linear-gradient(248deg, #75ff98 5.66%, #3dff54 42.49%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
"
>
Review passed
</div>
<!-- 未通过 -->
<div
v-if="bankCardInfo?.status === 'NOT_PASS'"
style="
font-weight: 510;
font-size: 0.9em;
background: linear-gradient(248deg, #ff7578 5.66%, #ff3d40 42.49%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
"
>
Review failed
</div>
<!-- 审核中 -->
<div
v-if="bankCardInfo?.status === 'PENDING'"
style="
font-weight: 510;
font-size: 0.9em;
background: linear-gradient(248deg, #ffe675 5.66%, #ffc53d 42.49%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
"
>
Awaiting review
</div>
</div>
<!-- 银行卡信息 -->
<!-- 资料内容 -->
<div
style="
background-color: white;
@ -337,9 +384,9 @@
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
"
>
<!-- 未上传银行卡信息 -->
<!-- 未上传 -->
<div
v-if="!hasBankCard"
v-if="!hasKYC"
style="display: flex; justify-content: space-between; align-items: center"
@click="gotoKYC"
>
@ -347,12 +394,12 @@
<img src="../../assets/icon/arrow.png" alt="" style="width: 6vw" class="flipImg" />
</div>
<!-- 获取到银行卡信息 -->
<!-- 获取到上传的资料 -->
<div v-else style="width: 100%; display: flex; flex-direction: column; gap: 8px">
<!-- 图片 -->
<div style="display: flex; justify-content: space-between; align-items: center">
<span style="font-weight: 510; font-size: 1em">{{
t('passport_id_card', { current: bankCardInfo.cardImages.length })
t('passport_id_card', { current: bankCardInfo.previewUrls.length })
}}</span>
<img src="../../assets/icon/arrow.png" alt="" style="width: 6vw" @click="gotoKYC" />
</div>
@ -360,7 +407,7 @@
<img
:src="imgUrl"
alt=""
v-for="imgUrl in bankCardInfo.cardImages"
v-for="imgUrl in bankCardInfo.previewUrls"
style="width: 10vw; aspect-ratio: 1/1; border-radius: 4px"
/>
</div>
@ -374,13 +421,13 @@
<!-- 备注信息 -->
<div style="font-weight: 500">{{ t('other_description') }}:</div>
<div style="color: rgba(0, 0, 0, 0.4); font-weight: 600; font-size: 0.8em">
{{ bankCardInfo.description }}
{{ bankCardInfo.otherDescription }}
</div>
</div>
</div>
</div>
<!-- 提现按钮 -->
<!-- 展示金额填写弹窗按钮 -->
<div style="display: flex; justify-content: center; align-items: center">
<div
style="
@ -393,9 +440,10 @@
text-align: center;
"
:style="{
background: hasBankCard
? 'linear-gradient(135deg, #BB92FF 2.82%, #8B45FF 99.15%), linear-gradient(135deg, #D1CED6 2.82%, #7C7882 99.15%)'
: 'linear-gradient(135deg, #D1CED6 2.82%, #7C7882 99.15%)',
background:
bankCardInfo?.status === 'PASS'
? 'linear-gradient(135deg, #BB92FF 2.82%, #8B45FF 99.15%), linear-gradient(135deg, #D1CED6 2.82%, #7C7882 99.15%)'
: 'linear-gradient(135deg, #D1CED6 2.82%, #7C7882 99.15%)',
}"
@click="CashOut"
>
@ -406,9 +454,10 @@
</div>
<!-- 弹窗遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="maskLayerShow = false">
<!-- 输入邀请码模块 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="closedPopup">
<!-- 兑换弹窗 -->
<div
v-if="exchangeTipShow"
style="
width: 100vw;
height: 100vh;
@ -416,7 +465,223 @@
justify-content: center;
align-items: center;
"
@click="maskLayerShow = false"
@click="closedPopup"
>
<div
style="
width: 80%;
border-radius: 12px;
background: #fff;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
"
@click.stop
>
<div style="color: rgba(0, 0, 0, 0.8); font-weight: 800">{{ t('tips') }}</div>
<div style="color: rgba(0, 0, 0, 0.4); font-weight: 600; font-size: 0.7em">
Are you sure you want to exchangecoins?
</div>
<div
style="width: 80%; display: flex; justify-content: space-between; align-items: center"
>
<div
style="
display: flex;
justify-content: center;
align-items: center;
border-radius: 12px;
background: #fff;
border: 1px solid #e6e6e6;
color: rgba(0, 0, 0, 0.4);
font-weight: 800;
padding: 8px 12px;
width: 40%;
"
@click="closedPopup"
>
Cancel
</div>
<div
style="
display: flex;
justify-content: center;
align-items: center;
border-radius: 12px;
background: linear-gradient(
152deg,
rgba(198, 112, 255, 0.8) 7.01%,
rgba(119, 38, 255, 0.8) 92.99%
);
color: #fff;
font-weight: 800;
padding: 8px 12px;
width: 40%;
"
@click="handleExchange"
>
Confirm
</div>
</div>
</div>
</div>
<!-- 资料不足提示弹窗 -->
<div
v-if="cashOutTipShow"
style="
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
"
@click="closedPopup"
>
<div
style="
width: 80%;
border-radius: 12px;
background: #fff;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
"
@click.stop
>
<div style="color: rgba(0, 0, 0, 0.8); font-weight: 800">{{ t('tips') }}</div>
<div style="color: rgba(0, 0, 0, 0.4); font-weight: 600; font-size: 0.8em">
Please fill in the information carefully. You can only withdraw cash after the
information is completed.
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-weight: 600; font-size: 0.6em">
*lf there is a problem with the card number and the transfer cannot be made, it will
result in the review being.
</div>
<div
style="
display: flex;
justify-content: center;
align-items: center;
border-radius: 12px;
background: linear-gradient(
152deg,
rgba(198, 112, 255, 0.8) 7.01%,
rgba(119, 38, 255, 0.8) 92.99%
);
color: #fff;
font-weight: 800;
padding: 8px 12px;
width: 50%;
"
@click="gotoKYC"
>
{{ t('receive') }}
</div>
</div>
</div>
<!-- 选择提现方式抽屉 -->
<div
v-if="cashOutTypeShow"
style="position: fixed; bottom: 0; width: 100%"
@click="closedPopup"
>
<div
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px;
"
@click.stop
>
<div style="display: flex; justify-content: center; align-items: center">
<div style="margin: 0; font-size: 18px; font-weight: 600; color: #333">Cash out</div>
</div>
<!-- 选择支付方式 -->
<div style="box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); border-radius: 8px; padding: 12px">
<div style="font-size: 0.9em; font-weight: 600; margin-bottom: 4px">
Withdrawal Method: {{ usedBankCard?.cardType || '' }}
</div>
<div
style="display: flex; justify-content: space-between; align-items: center"
@click="gotoselectBank"
>
<div style="font-size: 0.9em; font-weight: 600">Please select a bank card</div>
<img src="../../assets/icon/arrow.png" alt="" style="width: 6vw" class="flipImg" />
</div>
</div>
<!-- 输入提现金额 -->
<div style="box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); border-radius: 8px; padding: 12px">
<input
type="number"
min="50"
name=""
id=""
placeholder="Please enter the cash out amount"
style="
width: 100%;
outline: none;
border: none;
background-color: transparent;
color: black;
"
v-model="cashOutAmount"
/>
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-weight: 600; font-size: 0.9em">
*The cash out amount must be >= $50
</div>
<div style="display: flex; justify-content: center; align-items: center">
<div
style="
margin-top: 10px;
width: 80%;
border-radius: 32px;
padding: 12px;
color: #fff;
font-weight: 590;
text-align: center;
"
:style="{
background: checkStatus
? 'linear-gradient(135deg, #BB92FF 2.82%, #8B45FF 99.15%), linear-gradient(135deg, #D1CED6 2.82%, #7C7882 99.15%)'
: 'linear-gradient(135deg, #D1CED6 2.82%, #7C7882 99.15%)',
}"
@click="Receive"
>
{{ t('cash_out') }}
</div>
</div>
</div>
</div>
<!-- 等待审核提示弹窗 -->
<div
v-if="cashOutShow"
style="
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
"
@click="closedPopup"
>
<div
style="
@ -452,7 +717,7 @@
padding: 8px 12px;
width: 50%;
"
@click="Receive"
@click="closedPopup"
>
{{ t('receive') }}
</div>
@ -472,12 +737,13 @@ import {
userBankTransfer,
userSalaryCheckExchange,
userBankExchangeGold,
getBankCardList, //使
getWithdrawInfoList, //
withdrawApply, //
} from '@/api/wallet.js'
import { getSelectedPayee, clearSelectedPayee } from '@/utils/payeeStore.js'
import { showError, showSuccess } from '@/utils/toast.js'
import { setApplyInfo, getApplyInfo } from '@/utils/applyStore.js'
import maskLayer from '@/components/MaskLayer.vue'
import { withdrawApply } from '@/api/bdCenter.js'
import { getUserId } from '@/utils/userStore.js'
import { setDocumentDirection } from '@/locales/i18n'
@ -493,7 +759,25 @@ const selectedExchangeOption = ref(null)
const activeAction = ref('Exchange')
const loading = ref(false)
const maskLayerShow = ref(false)
const exchangeTipShow = ref(false)
const cashOutTipShow = ref(false)
const cashOutTypeShow = ref(false)
const cashOutShow = ref(false)
const cashOutAmount = ref(null)
const maskLayerShow = computed(() => {
return exchangeTipShow.value || cashOutTipShow.value || cashOutShow.value || cashOutTypeShow.value
})
const usedBankCard = ref(null) //使
const closedPopup = () => {
exchangeTipShow.value = false
cashOutTipShow.value = false
cashOutShow.value = false
cashOutTypeShow.value = false
}
//
const actions = ref([
@ -502,19 +786,10 @@ const actions = ref([
{ name: 'Cash out', lang: 'cash_out' },
])
//
const gotoKYC = () => {
router.push('/KYC')
}
//
const CashOut = () => {
if (hasBankCard.value) {
try {
maskLayerShow.value = true
} catch (error) {
showError(error.errorMsg)
}
if (hasKYC.value && bankCardInfo.value?.status === 'PASS') {
cashOutTypeShow.value = true
}
}
@ -550,11 +825,45 @@ const exchangeOptions = reactive([
{ id: 6, coins: 2100000, cash: 200 },
])
const bankCardInfo = ref({})
const hasBankCard = computed(() => {
return Object.keys(bankCardInfo.value).length > 0
const bankCardInfo = ref(null)
const hasKYC = computed(() => {
return bankCardInfo.value != null
})
//
const getWithdrawInfoListData = async () => {
try {
const resWithdrawInfoList = await getWithdrawInfoList()
console.log('提现信息列表:', resWithdrawInfoList)
if (resWithdrawInfoList.status && resWithdrawInfoList.body?.length > 0) {
let imgList = JSON.parse(resWithdrawInfoList.body[0].passportFrontUrl)
let previewUrls = imgList.map((item) => item)
bankCardInfo.value = { previewUrls, ...resWithdrawInfoList.body[0] }
}
} catch (error) {
console.error('获取提现信息列表失败:', error)
showError(error.response?.errorMsg)
}
}
//
const getBankCards = async () => {
try {
const resBankCards = await getBankCardList()
console.log('银行卡列表:', resBankCards)
if (resBankCards.status && resBankCards.body) {
//
resBankCards.body.forEach((card) => {
if (card.use) {
usedBankCard.value = card
}
})
}
} catch (error) {
console.error('获取银行卡列表失败:', error)
}
}
//
const initializePayee = () => {
const savedPayee = getSelectedPayee()
@ -584,11 +893,22 @@ const fetchBankBalance = async () => {
}
}
//
//
const gotoKYC = () => {
router.push('/KYC')
}
//
const gotoselectBank = () => {
router.push('/bank-card')
}
//
const showDetails = () => {
router.push('/income-details')
}
//
const searchPayee = () => {
router.push('/search-payee')
}
@ -616,6 +936,7 @@ const clearPayee = () => {
clearSelectedPayee()
}
//
const selectCoin = (coin) => {
if (selectedCoin.value === coin.id) {
selectedCoin.value = null
@ -624,6 +945,7 @@ const selectCoin = (coin) => {
}
}
//
const selectExchangeOption = (option) => {
if (selectedExchangeOption.value === option.id) {
selectedExchangeOption.value = null
@ -632,6 +954,7 @@ const selectExchangeOption = (option) => {
}
}
//
const transfer = async () => {
if (!selectedCoin.value) {
showError(t('please_select_amount'))
@ -668,6 +991,7 @@ const transfer = async () => {
}
}
//
const handleExchange = async () => {
if (!selectedExchangeOption.value) {
showError(t('please_select_coin_amount'))
@ -700,33 +1024,39 @@ const handleExchange = async () => {
}
}
//
const checkStatus = computed(() => {
return bankCardInfo.value?.status == 'PASS' && usedBankCard.value?.id && cashOutAmount.value > 50
})
//
const Receive = async () => {
try {
const resApply = await withdrawApply({ ...getApplyInfo(), activityId: getUserId() })
if (resApply.status) {
setApplyInfo()
if (checkStatus.value) {
try {
const resApply = await withdrawApply({
amount: cashOutAmount.value,
acceptBankCardId: usedBankCard.value.id,
})
if (resApply.status) {
cashOutShow.value = true
}
} catch (error) {
cashOutShow.value = false
showError(error.response?.errorMsg || t('something_went_wrong'))
}
maskLayerShow.value = false
} catch (error) {
maskLayerShow.value = false
showError(error.response?.errorMsg || t('something_went_wrong'))
}
}
//
onMounted(() => {
fetchBankBalance()
initializePayee()
if (getApplyInfo()) {
bankCardInfo.value =
{ ...getApplyInfo(), cardImages: JSON.parse(getApplyInfo().cardImages) } || {}
}
fetchBankBalance() //
initializePayee() //
getWithdrawInfoListData() //
getBankCards() // 使
})
onUnmounted(() => {
clearPayee()
// setApplyInfo()
})
</script>
@ -736,6 +1066,11 @@ onUnmounted(() => {
font-family: 'SF Pro Text';
}
input::placeholder {
font-weight: 510;
color: rgba(0, 0, 0, 0.4);
}
.transfer {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
width: 100vw;

View File

@ -0,0 +1,536 @@
<template>
<div class="fullPage">
<GeneralHeader style="width: 100%" title="Bank Card" :showLanguageList="true" color="black" />
<div style="padding: 16px; display: flex; flex-direction: column; gap: 8px">
<!-- 当前使用的银行卡 -->
<div style="font-weight: 600">Currently In Use</div>
<div
style="
border-radius: 8px;
background: #fff;
padding: 12px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
"
>
<div v-if="!usedBankCard.cardNo" style="font-weight: 510; color: rgba(0, 0, 0, 0.4)">
Bank cards are not currently in use.
</div>
<div v-else style="display: flex">
<div style="width: 15%; display: flex; justify-content: center; align-items: center">
<img
src="/src/assets/icon/coin.png"
alt=""
style="display: block; width: 100%; object-fit: cover"
/>
</div>
<div style="display: flex; flex-direction: column; justify-content: space-around">
<div style="font-weight: 510; color: rgba(0, 0, 0, 0.4)">
Card Number:{{ usedBankCard.cardNo }}
</div>
<div style="font-weight: 510; color: rgba(0, 0, 0, 0.4)">
Payee:{{ usedBankCard.payee }}
</div>
</div>
</div>
</div>
<!-- 所拥有的银行卡 -->
<div style="font-weight: 600">All Bank Cards</div>
<!-- 列表为空 -->
<div
v-if="bankCardList.length === 0"
style="
border-radius: 8px;
background: #fff;
padding: 12px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
"
>
<div style="font-weight: 510; color: rgba(0, 0, 0, 0.4)">
No bank cards have been added yet.
</div>
</div>
<!-- 列表不为空 -->
<div
v-else
v-for="card in bankCardList"
style="
border-radius: 8px;
background: #fff;
padding: 12px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
"
>
<div style="display: flex; align-items: center; gap: 4px">
<div style="width: 15%">
<img
src="/src/assets/icon/coin.png"
alt=""
style="display: block; width: 100%; object-fit: cover"
/>
</div>
<div style="display: flex; flex-direction: column; gap: 4px">
<div style="font-weight: 510; color: rgba(0, 0, 0, 0.4)">
Card Number:{{ card.cardNo }}
</div>
<div style="font-weight: 510; color: rgba(0, 0, 0, 0.4)">Payee:{{ card.payee }}</div>
<div style="display: flex; align-items: center; gap: 8px">
<div
style="
font-size: 0.9em;
font-weight: 700;
padding: 2px 12px;
color: #fff;
border-radius: 16px;
background: radial-gradient(
107.94% 107.94% at 15.9% 13.54%,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
linear-gradient(180deg, #fff 0%, #000 100%), #ff6464;
background-blend-mode: normal, soft-light, normal;
"
@click="removeBankCard(card.id)"
>
Delet
</div>
<div
style="
font-size: 0.9em;
font-weight: 700;
padding: 2px 12px;
color: #fff;
border-radius: 16px;
background: radial-gradient(
107.94% 107.94% at 15.9% 13.54%,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
linear-gradient(180deg, #fff 0%, #000 100%), #b464ff;
background-blend-mode: normal, soft-light, normal;
"
@click="setDefaultBankCard(card.id)"
>
Use
</div>
</div>
</div>
</div>
</div>
</div>
<div style="height: 50px"></div>
<div
style="
position: fixed;
width: 100%;
bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
"
>
<div
style="
padding: 12px;
border-radius: 32px;
background: linear-gradient(135deg, #bb92ff 2.82%, #8b45ff 99.15%),
linear-gradient(135deg, #d1ced6 2.82%, #7c7882 99.15%);
display: flex;
align-items: center;
gap: 4px;
"
@click="maskLayerShow = true"
>
<div style="width: 5vw; display: flex; justify-content: center; align-items: center">
<img
src="/src/assets/icon/addWhite.png"
alt=""
style="display: block; width: 100%; object-fit: cover"
/>
</div>
<div style="color: #fff; font-size: 0.9em">Add new payment method</div>
</div>
</div>
<!-- 弹窗遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="maskLayerShow = false">
<!-- 选择提现方式抽屉 -->
<div style="position: fixed; bottom: 0; width: 100%">
<div
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px;
"
@click.stop
>
<!-- 标题 -->
<div style="display: flex; justify-content: center; align-items: center">
<div style="font-size: 1.1em; font-weight: 600; color: #333">
Add new payment method
</div>
</div>
<!-- 添加付款方式 -->
<div style="font-size: 0.9em; font-weight: 600; margin-bottom: 4px">
Withdrawal Method:
</div>
<div>
<div
style="box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); border-radius: 8px; padding: 12px"
>
<div
style="display: flex; justify-content: space-between; align-items: center"
@click="withdrawelMethodShow = !withdrawelMethodShow"
>
<div v-if="selectedMethod.cardType" style="font-size: 0.9em; font-weight: 500">
{{ selectedMethod.cardType }}
</div>
<div v-else style="font-size: 0.9em; font-weight: 500; color: rgba(0, 0, 0, 0.4)">
Choose withdrawel method
</div>
<img
src="../../../assets/icon/arrow.png"
alt=""
style="display: block; width: 6vw; transition: transform 0.5s"
class="flipImg"
:class="{ rotated: withdrawelMethodShow }"
/>
</div>
</div>
<div v-show="withdrawelMethodShow" style="position: relative; top: -15px">
<div
style="
position: absolute;
width: 100%;
left: 0;
top: 0;
max-height: 50vw;
background-color: #fff;
border-radius: 0 0 16px 16px;
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
"
class="overflow-y-auto"
>
<div
v-for="way in withdrawalWaysList"
style="font-size: 0.8em"
@click="selectMethod(way)"
>
{{ way }}
</div>
</div>
</div>
</div>
<!-- 输入银行卡号 -->
<div style="font-size: 0.9em; font-weight: 600; margin-bottom: 4px">Card Number:</div>
<div style="box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); border-radius: 8px; padding: 12px">
<input
type="text"
name=""
id=""
placeholder="Enter the Card Number"
style="
width: 100%;
outline: none;
border: none;
background-color: transparent;
color: black;
"
v-model="selectedMethod.cardNo"
/>
</div>
<!-- 输入收款人 -->
<div style="font-size: 0.9em; font-weight: 600; margin-bottom: 4px">Payee</div>
<div style="box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); border-radius: 8px; padding: 12px">
<input
type="text"
name=""
id=""
placeholder="Enter the payee"
style="
width: 100%;
outline: none;
border: none;
background-color: transparent;
color: black;
"
v-model="selectedMethod.payee"
/>
</div>
<div style="display: flex; justify-content: center; align-items: center">
<div
style="
margin-top: 10px;
border-radius: 32px;
padding: 12px 20px;
color: #fff;
font-weight: 590;
text-align: center;
"
:style="{
background: checkpassed
? 'linear-gradient(135deg, #BB92FF 2.82%, #8B45FF 99.15%), linear-gradient(135deg, #D1CED6 2.82%, #7C7882 99.15%)'
: 'linear-gradient(135deg, #D1CED6 2.82%, #7C7882 99.15%)',
}"
@click="Confirm"
>
Confirm
</div>
</div>
</div>
</div>
</maskLayer>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
import { showError, showWarning, showInfo, showSuccess } from '@/utils/toast.js'
import { getUserId } from '@/utils/userStore'
import {
getWithdrawalWays,
getBankCardList,
addBankCard,
deleteBankCard,
useBankCard,
} from '@/api/wallet'
import maskLayer from '@/components/MaskLayer.vue'
const { t } = useI18n()
const maskLayerShow = ref(false)
const withdrawelMethodShow = ref(false)
const withdrawalWaysList = ref([]) //
const bankCardList = ref([]) //
// 使
const usedBankCard = ref({
cardType: '',
cardName: '',
cardNo: '',
payee: '',
})
//
const selectedMethod = ref({
userId: getUserId(),
cardType: '',
cardName: '',
cardNo: '',
payee: '',
})
//
const checkpassed = computed(() => {
return selectedMethod.value.cardType && selectedMethod.value.cardNo && selectedMethod.value.payee
})
//
const selectMethod = (way) => {
selectedMethod.value.cardType = way
selectedMethod.value.cardName = way
withdrawelMethodShow.value = !withdrawelMethodShow.value
}
//
const Confirm = async () => {
if (checkpassed.value) {
//
const resAddCard = await addBankCard(selectedMethod.value)
if (resAddCard.status) {
//
maskLayerShow.value = false
selectedMethod.value = {
...selectedMethod.value,
cardType: '',
cardName: '',
cardNo: '',
payee: '',
}
getBankCards()
} else {
//
console.log('添加银行卡失败:', resAddCard.message)
}
}
}
//
const getWithdrawalWaysList = async () => {
try {
const resWithdrawalWays = await getWithdrawalWays()
console.log('提现方式列表:', resWithdrawalWays)
if (resWithdrawalWays.status && resWithdrawalWays.body) {
//
withdrawalWaysList.value = resWithdrawalWays.body
}
} catch (error) {
console.error('获取提现方式列表失败:', error)
}
}
//
const getBankCards = async () => {
try {
const resBankCards = await getBankCardList()
console.log('银行卡列表:', resBankCards)
if (resBankCards.status && resBankCards.body) {
//
bankCardList.value = resBankCards.body
bankCardList.value.forEach((card) => {
if (card.use) {
usedBankCard.value = card
}
})
}
} catch (error) {
console.error('获取银行卡列表失败:', error)
}
}
//
const removeBankCard = async (cardId) => {
try {
const resDeleteCard = await deleteBankCard({ id: cardId })
if (resDeleteCard.status) {
getBankCards() //
usedBankCard.value = {
cardType: '',
cardName: '',
cardNo: '',
payee: '',
}
}
} catch (error) {
showError(error.response?.errorMsg)
}
}
//
const setDefaultBankCard = async (cardId) => {
try {
const resUseCard = await useBankCard({ id: cardId })
if (resUseCard.status) {
getBankCards() //
}
} catch (error) {
showError(error.response?.errorMsg)
}
}
//
const addNewWithdrawInfo = async (infoData) => {
try {
const resAddInfo = await addWithdrawInfo(infoData)
if (resAddInfo.status) {
showSuccess(t('withdraw_info_added'))
getWithdrawInfo() //
}
} catch (error) {
showError(error.response?.errorMsg || t('add_withdraw_info_failed'))
}
}
//
const updateWithdrawInfoData = async (infoData) => {
try {
const resUpdateInfo = await updateWithdrawInfo(infoData)
if (resUpdateInfo.status) {
showSuccess(t('withdraw_info_updated'))
getWithdrawInfo() //
}
} catch (error) {
showError(error.response?.errorMsg || t('update_withdraw_info_failed'))
}
}
//
const initData = () => {
getWithdrawalWaysList() //
getBankCards() //
}
onMounted(() => {
initData()
})
</script>
<style lang="scss" scoped>
* {
color: rgba(0, 0, 0, 0.8);
font-family: 'SF Pro Text';
}
.fullPage {
width: 100vw;
min-height: 100vh;
background-color: #fff;
background-image: url(/src/assets/images/secondBg.png);
background-size: 100% auto;
background-repeat: no-repeat;
position: relative;
}
.overflow-y-auto {
overflow-y: auto;
}
.overflow-y-auto::-webkit-scrollbar {
display: none;
}
.rotated {
transform: rotate(90deg);
}
input::placeholder {
color: rgba(0, 0, 0, 0.4);
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 14px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>

View File

@ -15,7 +15,7 @@
>
<input
type="text"
v-model="cardNum"
v-model="applyInfo.contactNumber"
:placeholder="t('enter_contact_number')"
style="width: 100%; border: none; outline: none"
/>
@ -33,11 +33,10 @@
"
>
<textarea
v-model="description"
v-model="applyInfo.otherDescription"
rows="5"
maxlength="100"
:placeholder="t('enter_other_description')"
@input="updateCount"
style="width: 100%; border: none; outline: none; resize: none"
></textarea>
<span
@ -97,6 +96,50 @@
</div>
</div>
<!-- 当前状态 -->
<div v-if="updateStatus" style="display: flex; align-items: flex-end; gap: 8px">
<div style="font-weight: 600">Current status:</div>
<div
v-if="applyInfo.status === 'PASS'"
style="
font-weight: 510;
font-size: 0.9em;
background: linear-gradient(248deg, #75ff98 5.66%, #3dff54 42.49%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
"
>
Review passed
</div>
<div
v-else-if="applyInfo.status === 'NOT_PASS'"
style="
font-weight: 510;
font-size: 0.9em;
background: linear-gradient(248deg, #ff7578 5.66%, #ff3d40 42.49%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
"
>
Review failed
</div>
<div
v-else-if="applyInfo.status === 'PENDING'"
style="
font-weight: 510;
font-size: 0.9em;
background: linear-gradient(248deg, #ffe675 5.66%, #ffc53d 42.49%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
"
>
Awaiting review
</div>
</div>
<!-- 提现按钮 -->
<div style="display: flex; justify-content: center; align-items: center">
<div
@ -125,10 +168,10 @@ import { useI18n } from 'vue-i18n'
import GeneralHeader from '@/components/GeneralHeader.vue'
import { ref, onMounted, computed } from 'vue'
import { showError, showWarning, showInfo, showSuccess } from '@/utils/toast.js'
import { setApplyInfo, getApplyInfo } from '@/utils/applyStore.js'
import { useRouter } from 'vue-router'
import { uploadImg } from '@/api/upload.js'
import { setDocumentDirection } from '@/locales/i18n'
import { getWithdrawInfoList, addWithdrawInfo, updateWithdrawInfo } from '@/api/wallet'
const { t, locale } = useI18n()
const router = useRouter()
@ -136,33 +179,41 @@ const router = useRouter()
//
locale.value && setDocumentDirection(locale.value)
const cardNum = ref('')
const description = ref('')
const count = computed(() => description.value.length)
const count = computed(() => applyInfo.value.otherDescription.length) //
const previewUrls = ref([]) //
const tempFiles = ref([]) //
const resImgUrls = ref([]) //
const tempFiles = ref([]) //
const previewUrls = ref([]) //
const resImgUrls = ref([]) //
//
const updateCount = () => {
// @input
}
const updateStatus = ref(false) //
//
//
const applyInfo = ref({
id: '', //ID
contactNumber: '', //
category: 'CARD', //
otherDescription: '', //
passportFrontUrl: '', //
passportBackUrl: 'meaningless',
})
//
const checkStatus = computed(() => {
return applyInfo.value.contactNumber != '' && previewUrls.value.length > 0
})
// ()
const handleFileChange = (e) => {
//3
// 3
if (previewUrls.value.length < 3) {
const fileList = e.target.files //
// 1.
//
if (!fileList || fileList.length === 0) {
console.error('未选择文件')
return
}
// 2.
//
const file = fileList[0]
if (!file.type || !file.type.startsWith('image/')) {
console.error(`不支持的文件类型: ${file.type || '未知'}`)
@ -179,7 +230,6 @@ const handleFileChange = (e) => {
previewUrls.value.push(URL.createObjectURL(file))
//
tempFiles.value.push(file)
// uploadImgUrl()
} else {
showWarning(t('max_images_warning'))
}
@ -208,22 +258,75 @@ const uploadImgUrl = async () => {
//
const Submit = async () => {
if (!cardNum.value) {
//
console.log('checkStatus.value:', checkStatus.value)
if (checkStatus.value) {
await uploadImgUrl()
applyInfo.value.passportFrontUrl = JSON.stringify(resImgUrls.value)
if (updateStatus.value) {
console.log('更新信息:', applyInfo.value)
updateWithdrawInfoData()
} else {
console.log('添加信息:', applyInfo.value)
addWithdrawInfoData()
}
// router.go(-1)
} else {
showError(t('please_enter_contact_number'))
return
}
await uploadImgUrl()
let data = {
amount: 10,
contactNumber: cardNum.value,
description: description.value,
cardImages: JSON.stringify(resImgUrls.value),
}
setApplyInfo(data)
router.go(-1)
showSuccess(t('application_submitted'))
}
//
const getWithdrawInfoListData = async () => {
try {
const resWithdrawInfoList = await getWithdrawInfoList()
console.log('提现信息列表:', resWithdrawInfoList)
if (resWithdrawInfoList.status && resWithdrawInfoList.body?.length > 0) {
updateStatus.value = true
applyInfo.value = resWithdrawInfoList.body[0]
let imgList = JSON.parse(resWithdrawInfoList.body[0].passportFrontUrl)
previewUrls.value = imgList.map((item) => item)
resImgUrls.value = imgList.map((item) => item)
}
} catch (error) {
console.error('获取提现信息列表失败:', error)
showError(error.response?.errorMsg)
}
}
//
const addWithdrawInfoData = async () => {
try {
const resAddWithdrawInfo = await addWithdrawInfo(applyInfo.value)
if (resAddWithdrawInfo.status) {
showSuccess(t('application_submitted'))
getWithdrawInfoListData() //
}
} catch (error) {
showError(error.response?.errorMsg)
throw error
}
}
//
const updateWithdrawInfoData = async () => {
try {
const resUpdateWithdrawInfo = await updateWithdrawInfo(applyInfo.value)
if (resUpdateWithdrawInfo.status) {
showSuccess(t('application_submitted'))
getWithdrawInfoListData() //
}
} catch (error) {
showError(error.response?.errorMsg)
throw error
}
}
onMounted(() => {
getWithdrawInfoListData() //
})
</script>
<style lang="scss" scoped>