feat: 添加银行账户和货币选择组件,优化提现页面交互体验

This commit is contained in:
2026-01-10 17:33:59 +07:00
parent 3cd91fcad0
commit 3111436054
8 changed files with 151 additions and 40 deletions

2
components.d.ts vendored
View File

@@ -58,6 +58,7 @@ declare module 'vue' {
IonTabs: typeof import('@ionic/vue')['IonTabs'] IonTabs: typeof import('@ionic/vue')['IonTabs']
IonText: typeof import('@ionic/vue')['IonText'] IonText: typeof import('@ionic/vue')['IonText']
IonTitle: typeof import('@ionic/vue')['IonTitle'] IonTitle: typeof import('@ionic/vue')['IonTitle']
IonToggle: typeof import('@ionic/vue')['IonToggle']
IonToolbar: typeof import('@ionic/vue')['IonToolbar'] IonToolbar: typeof import('@ionic/vue')['IonToolbar']
LayoutDefault: typeof import('./src/components/layout/default.vue')['default'] LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
PwaInstallButton: typeof import('./src/components/pwa-install-button/index.vue')['default'] PwaInstallButton: typeof import('./src/components/pwa-install-button/index.vue')['default']
@@ -117,6 +118,7 @@ declare global {
const IonTabs: typeof import('@ionic/vue')['IonTabs'] const IonTabs: typeof import('@ionic/vue')['IonTabs']
const IonText: typeof import('@ionic/vue')['IonText'] const IonText: typeof import('@ionic/vue')['IonText']
const IonTitle: typeof import('@ionic/vue')['IonTitle'] const IonTitle: typeof import('@ionic/vue')['IonTitle']
const IonToggle: typeof import('@ionic/vue')['IonToggle']
const IonToolbar: typeof import('@ionic/vue')['IonToolbar'] const IonToolbar: typeof import('@ionic/vue')['IonToolbar']
const LayoutDefault: typeof import('./src/components/layout/default.vue')['default'] const LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
const PwaInstallButton: typeof import('./src/components/pwa-install-button/index.vue')['default'] const PwaInstallButton: typeof import('./src/components/pwa-install-button/index.vue')['default']

View File

@@ -34,7 +34,7 @@ export type RwaIssuanceProductBody = TreatyBody<typeof client.api.rwa.issuance.p
export type RwaIssuanceCategoriesData = Treaty.Data<typeof client.api.rwa.category.categories.get>; export type RwaIssuanceCategoriesData = Treaty.Data<typeof client.api.rwa.category.categories.get>;
export type BankAccountsData = Treaty.Data<typeof client.api.bank_account.get>; export type BankAccountsData = Treaty.Data<typeof client.api.bank_account.get>["data"][number];
export type BankAccountBody = TreatyBody<typeof client.api.bank_account.post>; export type BankAccountBody = TreatyBody<typeof client.api.bank_account.post>;

View File

@@ -58,11 +58,11 @@
"chooseCurrency": "选择货币", "chooseCurrency": "选择货币",
"chooseMethod": "选择提现方式", "chooseMethod": "选择提现方式",
"amount": "金额", "amount": "金额",
"enterAmountMax": "请输入金额(最大:{amount}", "enterAmountMax": "请输入金额(最大可用{amount}",
"validAmountError": "请输入有效的金额。", "validAmountError": "请输入有效的金额。",
"bankAccountId": "银行账户ID", "bankAccountId": "银行账户",
"enterBankAccountId": "请输入银行账户ID", "enterBankAccountId": "请输入银行账户",
"validBankAccountError": "请输入有效的银行账户ID。", "validBankAccountError": "请输入有效的银行账户。",
"chooseChain": "选择链", "chooseChain": "选择链",
"cryptoAddress": "加密货币地址", "cryptoAddress": "加密货币地址",
"enterCryptoAddress": "请输入加密货币地址", "enterCryptoAddress": "请输入加密货币地址",

View File

@@ -30,9 +30,9 @@
"chooseCurrency": "選擇貨幣", "chooseCurrency": "選擇貨幣",
"chooseMethod": "選擇提現方式", "chooseMethod": "選擇提現方式",
"amount": "金額", "amount": "金額",
"enterAmountMax": "請輸入金額(最大:{amount}", "enterAmountMax": "請輸入金額(最大可用{amount}",
"validAmountError": "請輸入有效的金額。", "validAmountError": "請輸入有效的金額。",
"bankAccountId": "銀行賬戶ID", "bankAccountId": "銀行賬戶",
"enterBankAccountId": "請輸入銀行賬戶ID", "enterBankAccountId": "請輸入銀行賬戶ID",
"validBankAccountError": "請輸入有效的銀行賬戶ID。", "validBankAccountError": "請輸入有效的銀行賬戶ID。",
"chooseChain": "選擇鏈", "chooseChain": "選擇鏈",

View File

@@ -7,7 +7,7 @@ interface State {
balances: BalancesData; balances: BalancesData;
fundingBalances: BalancesData; fundingBalances: BalancesData;
tradingBalances: BalancesData; tradingBalances: BalancesData;
bankAccounts: BankAccountsData["data"]; bankAccounts: BankAccountsData[];
supportBanks: SupportBanksData["data"]; supportBanks: SupportBanksData["data"];
} }
@@ -57,7 +57,7 @@ export const useWalletStore = defineStore("wallet", () => {
state.tradingBalances = balances.value || []; state.tradingBalances = balances.value || [];
} }
async function syncBankAccounts(data?: BankAccountsData["data"]) { async function syncBankAccounts(data?: BankAccountsData[]) {
if (data) { if (data) {
state.bankAccounts = data; state.bankAccounts = data;
return; return;

View File

@@ -0,0 +1,39 @@
<script lang='ts' setup>
import type { BankAccountsData } from "@/api/types";
import { modalController } from "@ionic/vue";
const emit = defineEmits<{
select: [id: BankAccountsData];
}>();
const walletStore = useWalletStore();
const { bankAccounts } = storeToRefs(walletStore);
function handleSelect(item: BankAccountsData) {
emit("select", item);
modalController.dismiss(item);
}
</script>
<template>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ion-title>选择银行账户</ion-title>
</ion-toolbar>
<ion-content>
<div class="mt-2 flex flex-col">
<div v-for="item in bankAccounts" :key="item.id" class="flex items-center px-3 my-3 w-full" @click="handleSelect(item)">
<div class="ml-2 inline-flex gap-2 items-baseline">
<div class="w-10 h-10 rounded-xl flex items-center justify-center text-white font-bold text-xs shadow-sm bg-[#0F5AA6]">
<span class="text-white">{{ item.bankCode }}</span>
</div>
<span class="font-medium text-md">{{ item.bankName }}</span>
</div>
</div>
</div>
</ion-content>
</ion-header>
</template>
<style lang='css' scoped></style>

View File

@@ -0,0 +1,40 @@
<script lang='ts' setup>
import { modalController } from "@ionic/vue";
const emit = defineEmits<{
select: [code: string];
}>();
const walletStore = useWalletStore();
const { fundingBalances } = storeToRefs(walletStore);
function handleSelect(code: string) {
emit("select", code);
modalController.dismiss(code);
}
onMounted(() => {
walletStore.syncFundingBalances();
});
</script>
<template>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ion-title>选择货币</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="mt-2 flex flex-col">
<div v-for="item in fundingBalances" :key="item.assetCode" class="flex items-center px-3 my-3 w-full" @click="handleSelect(item.assetCode)">
<Icon :icon="item.asset.iconUrl || ''" class="text-3xl" />
<div class="ml-2 inline-flex gap-2 items-baseline">
<span class="font-medium text-md">{{ item.assetCode }}</span>
</div>
</div>
</div>
</ion-content>
</template>
<style lang='css' scoped></style>

View File

@@ -1,16 +1,21 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { GenericObject } from "vee-validate"; import type { GenericObject } from "vee-validate";
import type { WithdrawBody } from "@/api/types"; import type { BankAccountsData, WithdrawBody } from "@/api/types";
import { loadingController, toastController } from "@ionic/vue"; import type { FormInstance } from "@/utils";
import { loadingController, modalController, toastController } from "@ionic/vue";
import { ErrorMessage, Field, Form } from "vee-validate"; import { ErrorMessage, Field, Form } from "vee-validate";
import { client, safeClient } from "@/api"; import { client, safeClient } from "@/api";
import { AssetCodeEnum, ChainEnum, WithdrawMethodEnum } from "@/api/enum"; import { AssetCodeEnum, ChainEnum, WithdrawMethodEnum } from "@/api/enum";
import SelectBankAccount from "./components/select-bank-account.vue";
import SelectCurrency from "./components/select-currency.vue";
import { createWithdrawSchema } from "./rules"; import { createWithdrawSchema } from "./rules";
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const walletStore = useWalletStore(); const walletStore = useWalletStore();
const { fundingBalances, bankAccounts } = storeToRefs(walletStore); await walletStore.syncFundingBalances();
const { fundingBalances } = storeToRefs(walletStore);
const formInst = useTemplateRef<FormInstance>("formInst");
const initialValues: WithdrawBody = { const initialValues: WithdrawBody = {
assetCode: AssetCodeEnum.USDT, assetCode: AssetCodeEnum.USDT,
@@ -22,11 +27,42 @@ const initialValues: WithdrawBody = {
}; };
const maxAmount = computed(() => { const maxAmount = computed(() => {
const balance = fundingBalances.value?.find(item => item.assetCode === initialValues.assetCode); const balance = fundingBalances.value?.find(item => item.assetCode === (formInst.value?.getValues().assetCode || initialValues.assetCode));
return balance ? balance.available : "0"; return Number(balance ? balance.available : "0").toFixed(2);
}); });
const schema = computed(() => createWithdrawSchema(t, maxAmount.value)); const schema = computed(() => createWithdrawSchema(t, maxAmount.value));
const currentBankAccount = ref<BankAccountsData | null>(null);
async function openSelectCurrency() {
const modal = await modalController.create({
component: SelectCurrency,
componentProps: {
onSelect: (code: string) => {
formInst.value?.setFieldValue("assetCode", code);
},
},
breakpoints: [0, 0.8],
initialBreakpoint: 0.8,
handle: true,
});
await modal.present();
}
async function openSelectBankAccount() {
const modal = await modalController.create({
component: SelectBankAccount,
componentProps: {
onSelect: (item: BankAccountsData) => {
currentBankAccount.value = item;
formInst.value?.setFieldValue("bankAccountId", item.id);
},
},
breakpoints: [0, 0.8],
initialBreakpoint: 0.8,
handle: true,
});
await modal.present();
}
async function onSubmit(values: GenericObject) { async function onSubmit(values: GenericObject) {
const loading = await loadingController.create({ const loading = await loadingController.create({
@@ -61,6 +97,7 @@ async function onSubmit(values: GenericObject) {
</IonHeader> </IonHeader>
<IonContent :fullscreen="true" class="ion-padding"> <IonContent :fullscreen="true" class="ion-padding">
<Form <Form
ref="formInst"
:validation-schema="schema" :validation-schema="schema"
:initial-values="initialValues" :initial-values="initialValues"
@submit="onSubmit" @submit="onSubmit"
@@ -68,17 +105,13 @@ async function onSubmit(values: GenericObject) {
<div class="flex flex-col gap-5"> <div class="flex flex-col gap-5">
<div> <div>
<Field name="assetCode"> <Field name="assetCode">
<template #default="{ field, value }"> <template #default="{ value }">
<ion-radio-group v-bind="field" :model-value="value">
<ion-label class="text-sm"> <ion-label class="text-sm">
{{ t("withdraw.chooseCurrency") }} {{ t("withdraw.chooseCurrency") }}
</ion-label> </ion-label>
<ion-item v-for="item in AssetCodeEnum" :key="item"> <ion-item button class="ion-no-border" @click="openSelectCurrency">
<ion-radio :value="item" justify="space-between"> <ion-label>{{ value }}</ion-label>
{{ t(`withdraw.assetCode.${item}`) }}
</ion-radio>
</ion-item> </ion-item>
</ion-radio-group>
</template> </template>
</Field> </Field>
<ErrorMessage name="assetCode" class="text-red-500 text-xs mt-1" /> <ErrorMessage name="assetCode" class="text-red-500 text-xs mt-1" />
@@ -105,21 +138,18 @@ async function onSubmit(values: GenericObject) {
<Field name="withdrawMethod"> <Field name="withdrawMethod">
<template #default="{ value: withdrawMethod }"> <template #default="{ value: withdrawMethod }">
<div v-if="withdrawMethod === WithdrawMethodEnum.BANK"> <div v-if="withdrawMethod === WithdrawMethodEnum.BANK">
<Field name="bankAccountId" type="text"> <Field name="bankAccountId">
<template #default="{ field }"> <ion-label class="text-sm">
<ion-select {{ t('withdraw.bankAccountId') }}
v-bind="field" </ion-label>
class="ui-select" <ion-item button class="ion-no-border" @click="openSelectBankAccount">
interface="action-sheet" <template v-if="currentBankAccount">
toggle-icon="" <div class="w-8 h-8 rounded-md mr-2 flex items-center justify-center text-white font-bold text-xs shadow-sm bg-[#0F5AA6]">
:label="t('withdraw.bankAccountId')" <span class="text-white">{{ currentBankAccount?.bankCode }}</span>
:placeholder="t('withdraw.enterBankAccountId')" </div>
> <ion-label>{{ currentBankAccount?.bankName }}</ion-label>
<ion-select-option v-for="item in bankAccounts" :key="item.id" :value="item.id">
{{ item.bankName }} - {{ item.maskAccountNumber }}
</ion-select-option>
</ion-select>
</template> </template>
</ion-item>
</Field> </Field>
<ErrorMessage name="bankAccountId" class="text-red-500 text-xs mt-1" /> <ErrorMessage name="bankAccountId" class="text-red-500 text-xs mt-1" />
</div> </div>