feat: 添加提现表单验证规则,优化用户输入体验
This commit is contained in:
@@ -40,7 +40,19 @@
|
|||||||
"enterCryptoAddress": "Enter the crypto address",
|
"enterCryptoAddress": "Enter the crypto address",
|
||||||
"validCryptoAddressError": "Please enter a valid crypto address.",
|
"validCryptoAddressError": "Please enter a valid crypto address.",
|
||||||
"successMessage": "Withdrawal request submitted successfully!",
|
"successMessage": "Withdrawal request submitted successfully!",
|
||||||
"submit": "Submit"
|
"submit": "Submit",
|
||||||
|
"validation": {
|
||||||
|
"assetCodeRequired": "Please select a currency",
|
||||||
|
"amountRequired": "Please enter an amount",
|
||||||
|
"amountInvalid": "Please enter a valid number format",
|
||||||
|
"amountExceedsBalance": "Amount cannot exceed available balance",
|
||||||
|
"amountTooSmall": "Amount must be greater than 0",
|
||||||
|
"methodRequired": "Please select a withdrawal method",
|
||||||
|
"bankAccountRequired": "Please select a bank account",
|
||||||
|
"chainRequired": "Please select a blockchain network",
|
||||||
|
"addressRequired": "Please enter a withdrawal address",
|
||||||
|
"addressTooShort": "Address format is incorrect, too short"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"bankCard": {
|
"bankCard": {
|
||||||
"management": "Bank Card Management",
|
"management": "Bank Card Management",
|
||||||
|
|||||||
@@ -40,7 +40,19 @@
|
|||||||
"enterCryptoAddress": "请输入加密货币地址",
|
"enterCryptoAddress": "请输入加密货币地址",
|
||||||
"validCryptoAddressError": "请输入有效的加密货币地址。",
|
"validCryptoAddressError": "请输入有效的加密货币地址。",
|
||||||
"successMessage": "提现申请提交成功!",
|
"successMessage": "提现申请提交成功!",
|
||||||
"submit": "提交"
|
"submit": "提交",
|
||||||
|
"validation": {
|
||||||
|
"assetCodeRequired": "请选择货币",
|
||||||
|
"amountRequired": "请输入金额",
|
||||||
|
"amountInvalid": "请输入有效的数字格式",
|
||||||
|
"amountExceedsBalance": "金额不能超过可用余额",
|
||||||
|
"amountTooSmall": "金额必须大于0",
|
||||||
|
"methodRequired": "请选择提现方式",
|
||||||
|
"bankAccountRequired": "请选择银行账户",
|
||||||
|
"chainRequired": "请选择区块链网络",
|
||||||
|
"addressRequired": "请输入提现地址",
|
||||||
|
"addressTooShort": "地址格式不正确,长度过短"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"bankCard": {
|
"bankCard": {
|
||||||
"management": "银行卡管理",
|
"management": "银行卡管理",
|
||||||
|
|||||||
@@ -1,50 +1,35 @@
|
|||||||
<script lang='ts' setup>
|
<script lang='ts' setup>
|
||||||
|
import type { GenericObject } from "vee-validate";
|
||||||
import type { WithdrawBody } from "@/api/types";
|
import type { WithdrawBody } from "@/api/types";
|
||||||
import { toastController } from "@ionic/vue";
|
import { toastController } from "@ionic/vue";
|
||||||
|
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 { createWithdrawSchema } from "./rules";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
const walletStore = useWalletStore();
|
||||||
|
const { balances, bankAccounts } = storeToRefs(walletStore);
|
||||||
|
|
||||||
const amountInputInst = useTemplateRef<InputInstance>("amountInputInst");
|
const initialValues: WithdrawBody = {
|
||||||
const [form, resetForm] = useResetRef<WithdrawBody>({
|
|
||||||
assetCode: AssetCodeEnum.USDT,
|
assetCode: AssetCodeEnum.USDT,
|
||||||
amount: "",
|
amount: "",
|
||||||
withdrawMethod: WithdrawMethodEnum.BANK,
|
withdrawMethod: WithdrawMethodEnum.BANK,
|
||||||
toAddress: "",
|
toAddress: "",
|
||||||
bankAccountId: "",
|
bankAccountId: "",
|
||||||
chain: "BEP20",
|
chain: "BEP20",
|
||||||
});
|
};
|
||||||
const walletStore = useWalletStore();
|
|
||||||
const { balances, bankAccounts } = storeToRefs(walletStore);
|
|
||||||
const maxAmount = computed(() => {
|
const maxAmount = computed(() => {
|
||||||
const balance = balances.value?.find(item => item.assetCode === form.value.assetCode);
|
const balance = balances.value?.find(item => item.assetCode === initialValues.assetCode);
|
||||||
return balance ? balance.available : "0";
|
return balance ? balance.available : "0";
|
||||||
});
|
});
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
function markTouched() {
|
const schema = computed(() => createWithdrawSchema(t, maxAmount.value));
|
||||||
amountInputInst.value?.$el.classList.add("ion-touched");
|
|
||||||
}
|
|
||||||
function validate(value: string) {
|
|
||||||
amountInputInst.value?.$el.classList.remove("ion-valid");
|
|
||||||
amountInputInst.value?.$el.classList.remove("ion-invalid");
|
|
||||||
|
|
||||||
if (value === "") {
|
async function onSubmit(values: GenericObject) {
|
||||||
return false;
|
await safeClient(() => client.api.withdraw.post(values as WithdrawBody));
|
||||||
}
|
|
||||||
const isNumber = numberPattern.test(value);
|
|
||||||
|
|
||||||
isNumber ? amountInputInst.value?.$el.classList.add("ion-valid") : amountInputInst.value?.$el.classList.add("ion-invalid");
|
|
||||||
|
|
||||||
return isNumber;
|
|
||||||
}
|
|
||||||
function handleCurrentChange() {
|
|
||||||
form.value.amount = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSubmit() {
|
|
||||||
await safeClient(() => client.api.withdraw.post(form.value));
|
|
||||||
const toast = await toastController.create({
|
const toast = await toastController.create({
|
||||||
message: t("withdraw.successMessage"),
|
message: t("withdraw.successMessage"),
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
@@ -53,7 +38,6 @@ async function onSubmit() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await toast.present();
|
await toast.present();
|
||||||
resetForm();
|
|
||||||
router.back();
|
router.back();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -69,80 +53,127 @@ async function onSubmit() {
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
<IonContent :fullscreen="true" class="ion-padding">
|
<IonContent :fullscreen="true" class="ion-padding">
|
||||||
<div class="flex flex-col gap-5">
|
<Form
|
||||||
<ion-radio-group v-model="form.assetCode" @ion-change="handleCurrentChange">
|
:validation-schema="schema"
|
||||||
<ion-label class="text-sm">
|
:initial-values="initialValues"
|
||||||
{{ t("withdraw.chooseCurrency") }}
|
@submit="onSubmit"
|
||||||
</ion-label>
|
>
|
||||||
<ion-item v-for="item in AssetCodeEnum" :key="item">
|
<div class="flex flex-col gap-5">
|
||||||
<ion-radio :value="item" justify="space-between">
|
<div>
|
||||||
{{ item }}
|
<Field name="assetCode">
|
||||||
</ion-radio>
|
<template #default="{ field, value }">
|
||||||
</ion-item>
|
<ion-radio-group v-bind="field" :model-value="value">
|
||||||
</ion-radio-group>
|
<ion-label class="text-sm">
|
||||||
|
{{ t("withdraw.chooseCurrency") }}
|
||||||
|
</ion-label>
|
||||||
|
<ion-item v-for="item in AssetCodeEnum" :key="item">
|
||||||
|
<ion-radio :value="item" justify="space-between">
|
||||||
|
{{ item }}
|
||||||
|
</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
<ErrorMessage name="assetCode" class="text-red-500 text-xs mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<ion-radio-group v-model="form.withdrawMethod">
|
<div>
|
||||||
<ion-label class="text-sm">
|
<Field name="withdrawMethod">
|
||||||
{{ t("withdraw.chooseMethod") }}
|
<template #default="{ field, value }">
|
||||||
</ion-label>
|
<ion-radio-group v-bind="field" :model-value="value">
|
||||||
<ion-item v-for="item in WithdrawMethodEnum" :key="item">
|
<ion-label class="text-sm">
|
||||||
<ion-radio :value="item" justify="space-between">
|
{{ t("withdraw.chooseMethod") }}
|
||||||
{{ item }}
|
</ion-label>
|
||||||
</ion-radio>
|
<ion-item v-for="item in WithdrawMethodEnum" :key="item">
|
||||||
</ion-item>
|
<ion-radio :value="item" justify="space-between">
|
||||||
</ion-radio-group>
|
{{ item }}
|
||||||
|
</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
<ErrorMessage name="withdrawMethod" class="text-red-500 text-xs mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<ui-input-label
|
<Field name="withdrawMethod">
|
||||||
ref="amountInputInst"
|
<template #default="{ value: withdrawMethod }">
|
||||||
v-model="form.amount"
|
<div v-if="withdrawMethod === WithdrawMethodEnum.BANK">
|
||||||
:label="t('withdraw.amount')"
|
<Field name="bankAccountId">
|
||||||
:placeholder="t('withdraw.enterAmountMax', { amount: maxAmount })"
|
<template #default="{ field }">
|
||||||
type="number"
|
<ion-select
|
||||||
inputmode="numeric"
|
v-bind="field"
|
||||||
:error-text="t('withdraw.validAmountError')"
|
interface="action-sheet"
|
||||||
:max="maxAmount"
|
toggle-icon=""
|
||||||
@ion-input="validate($event.target.value as string)"
|
label-placement="floating"
|
||||||
@ion-blur="markTouched"
|
:label="t('withdraw.bankAccountId')"
|
||||||
/>
|
:placeholder="t('withdraw.enterBankAccountId')"
|
||||||
|
>
|
||||||
|
<ion-select-option v-for="item in bankAccounts" :key="item.id" :value="item.id">
|
||||||
|
{{ item.bankName }} - **** **** **** {{ item.accountName.slice(-4) }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
<ErrorMessage name="bankAccountId" class="text-red-500 text-xs mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<ion-select
|
<template v-else-if="withdrawMethod === WithdrawMethodEnum.CRYPTO">
|
||||||
v-if="form.withdrawMethod === WithdrawMethodEnum.BANK"
|
<div>
|
||||||
v-model="form.bankAccountId"
|
<Field name="chain">
|
||||||
interface="action-sheet"
|
<template #default="{ field, value }">
|
||||||
toggle-icon=""
|
<ion-radio-group v-bind="field" :model-value="value">
|
||||||
label-placement="floating"
|
<ion-label class="text-sm">
|
||||||
:label="t('withdraw.bankAccountId')"
|
{{ t("withdraw.chooseChain") }}
|
||||||
:placeholder="t('withdraw.enterBankAccountId')"
|
</ion-label>
|
||||||
>
|
<ion-item v-for="item in ChainEnum" :key="item">
|
||||||
<ion-select-option v-for="item in bankAccounts" :key="item.id" :value="item.id">
|
<ion-radio :value="item" justify="space-between">
|
||||||
{{ item.bankName }} - **** **** **** {{ item.accountName.slice(-4) }}
|
{{ item }}
|
||||||
</ion-select-option>
|
</ion-radio>
|
||||||
</ion-select>
|
</ion-item>
|
||||||
<template v-else-if="form.withdrawMethod === WithdrawMethodEnum.CRYPTO">
|
</ion-radio-group>
|
||||||
<ion-radio-group v-model="form.chain">
|
</template>
|
||||||
<ion-label class="text-sm">
|
</Field>
|
||||||
{{ t("withdraw.chooseChain") }}
|
<ErrorMessage name="chain" class="text-red-500 text-xs mt-1" />
|
||||||
</ion-label>
|
</div>
|
||||||
<ion-item v-for="item in ChainEnum" :key="item">
|
|
||||||
<ion-radio :value="item" justify="space-between">
|
|
||||||
{{ item }}
|
|
||||||
</ion-radio>
|
|
||||||
</ion-item>
|
|
||||||
</ion-radio-group>
|
|
||||||
<ui-input-label
|
|
||||||
v-model="form.toAddress"
|
|
||||||
:label="t('withdraw.cryptoAddress')"
|
|
||||||
:placeholder="t('withdraw.enterCryptoAddress')"
|
|
||||||
type="text"
|
|
||||||
inputmode="text"
|
|
||||||
:error-text="t('withdraw.validCryptoAddressError')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ion-button expand="block" @click="onSubmit">
|
<div>
|
||||||
{{ t("withdraw.submit") }}
|
<Field name="toAddress">
|
||||||
</ion-button>
|
<template #default="{ field }">
|
||||||
</div>
|
<ui-input-label
|
||||||
|
v-bind="field"
|
||||||
|
:label="t('withdraw.cryptoAddress')"
|
||||||
|
:placeholder="t('withdraw.enterCryptoAddress')"
|
||||||
|
type="text"
|
||||||
|
inputmode="text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
<ErrorMessage name="toAddress" class="text-red-500 text-xs mt-1" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Field name="amount">
|
||||||
|
<template #default="{ field }">
|
||||||
|
<ui-input-label
|
||||||
|
v-bind="field"
|
||||||
|
:label="t('withdraw.amount')"
|
||||||
|
:placeholder="t('withdraw.enterAmountMax', { amount: maxAmount })"
|
||||||
|
type="number"
|
||||||
|
inputmode="numeric"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
<ErrorMessage name="amount" class="text-red-500 text-xs mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-button expand="block" type="submit">
|
||||||
|
{{ t("withdraw.submit") }}
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</ion-page>
|
</ion-page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { toTypedSchema } from "@vee-validate/yup";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { WithdrawMethodEnum } from "@/api/enum";
|
||||||
|
|
||||||
|
export function createWithdrawSchema(t: (key: string, params?: any) => string, maxAmount: string) {
|
||||||
|
return toTypedSchema(
|
||||||
|
yup.object({
|
||||||
|
assetCode: yup.string().required(t("withdraw.validation.assetCodeRequired")),
|
||||||
|
amount: yup
|
||||||
|
.string()
|
||||||
|
.required(t("withdraw.validation.amountRequired"))
|
||||||
|
.test("is-number", t("withdraw.validation.amountInvalid"), value => {
|
||||||
|
return /^\d+(\.\d+)?$/.test(value || "");
|
||||||
|
})
|
||||||
|
.test("max-amount", t("withdraw.validation.amountExceedsBalance"), value => {
|
||||||
|
if (!value || maxAmount === "0") return false;
|
||||||
|
return parseFloat(value) <= parseFloat(maxAmount);
|
||||||
|
})
|
||||||
|
.test("min-amount", t("withdraw.validation.amountTooSmall"), value => {
|
||||||
|
if (!value) return false;
|
||||||
|
return parseFloat(value) > 0;
|
||||||
|
}),
|
||||||
|
withdrawMethod: yup.string().required(t("withdraw.validation.methodRequired")),
|
||||||
|
bankAccountId: yup.string().when("withdrawMethod", {
|
||||||
|
is: WithdrawMethodEnum.BANK,
|
||||||
|
then: (schema) => schema.required(t("withdraw.validation.bankAccountRequired")),
|
||||||
|
otherwise: (schema) => schema.optional(),
|
||||||
|
}),
|
||||||
|
chain: yup.string().when("withdrawMethod", {
|
||||||
|
is: WithdrawMethodEnum.CRYPTO,
|
||||||
|
then: (schema) => schema.required(t("withdraw.validation.chainRequired")),
|
||||||
|
otherwise: (schema) => schema.optional(),
|
||||||
|
}),
|
||||||
|
toAddress: yup.string().when("withdrawMethod", {
|
||||||
|
is: WithdrawMethodEnum.CRYPTO,
|
||||||
|
then: (schema) => schema.required(t("withdraw.validation.addressRequired")).min(10, t("withdraw.validation.addressTooShort")),
|
||||||
|
otherwise: (schema) => schema.optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user