276 lines
8.7 KiB
Vue
276 lines
8.7 KiB
Vue
<script lang='ts' setup>
|
|
import type { GenericObject } from "vee-validate";
|
|
import type { FormInstance } from "@/utils";
|
|
import { loadingController, modalController, toastController } from "@ionic/vue";
|
|
import { toTypedSchema } from "@vee-validate/zod";
|
|
import { swapVerticalOutline } from "ionicons/icons";
|
|
import { ErrorMessage, Field, Form } from "vee-validate";
|
|
import { z } from "zod";
|
|
import { client, safeClient } from "@/api";
|
|
import { AssetCodeEnum } from "@/api/enum";
|
|
import { getCryptoIcon } from "@/config/crypto";
|
|
import SelectCurrency from "../withdraw/components/select-currency.vue";
|
|
|
|
const { t } = useI18n();
|
|
const router = useRouter();
|
|
const walletStore = useWalletStore();
|
|
const formRef = useTemplateRef<FormInstance>("formRef");
|
|
|
|
type AccountType = "funding" | "trading";
|
|
|
|
interface TransferForm {
|
|
assetCode: AssetCodeEnum;
|
|
amount: string;
|
|
fromAccount: AccountType;
|
|
toAccount: AccountType;
|
|
}
|
|
|
|
const initialValues: TransferForm = {
|
|
assetCode: AssetCodeEnum.USDT,
|
|
amount: "",
|
|
fromAccount: "funding",
|
|
toAccount: "trading",
|
|
};
|
|
const availableBalance = ref("0");
|
|
|
|
// 验证规则
|
|
const schema = computed(() => toTypedSchema(z.object({
|
|
assetCode: z.string({ message: t("transfer.assetCodeRequired") }).min(1, t("transfer.assetCodeRequired")),
|
|
amount: z
|
|
.union([z.string(), z.number()])
|
|
.refine(value => !Number.isNaN(Number(value)) && Number(value) > 0, { message: t("transfer.amountMinError") })
|
|
.refine(value => Number(value) <= Number(availableBalance.value || 0), { message: t("transfer.amountMaxError", { amount: availableBalance.value }) }),
|
|
fromAccount: z.string({ message: t("transfer.fromAccountRequired") }).min(1, t("transfer.fromAccountRequired")),
|
|
toAccount: z.string({ message: t("transfer.toAccountRequired") }).min(1, t("transfer.toAccountRequired")),
|
|
})));
|
|
|
|
// 监听表单字段变化,自动更新可用余额
|
|
watch(() => formRef.value?.getValues(), (values) => {
|
|
if (values?.assetCode && values?.fromAccount) {
|
|
syncAvailableBalance();
|
|
}
|
|
}, { deep: true });
|
|
|
|
async function openSelectCurrency() {
|
|
const modal = await modalController.create({
|
|
component: SelectCurrency,
|
|
componentProps: {
|
|
onSelect: (code: string) => {
|
|
formRef.value?.setFieldValue("assetCode", code);
|
|
syncAvailableBalance();
|
|
},
|
|
},
|
|
breakpoints: [0, 0.8],
|
|
initialBreakpoint: 0.8,
|
|
handle: true,
|
|
});
|
|
await modal.present();
|
|
}
|
|
|
|
// 交换账户
|
|
function swapAccounts() {
|
|
const form = formRef.value as any;
|
|
if (!form)
|
|
return;
|
|
|
|
const currentFrom = form.values.fromAccount;
|
|
const currentTo = form.values.toAccount;
|
|
|
|
form.setFieldValue("fromAccount", currentTo);
|
|
form.setFieldValue("toAccount", currentFrom);
|
|
|
|
// 交换账户后重新获取余额
|
|
syncAvailableBalance();
|
|
}
|
|
|
|
// 设置全部金额
|
|
function setMaxAmount() {
|
|
const form = formRef.value as any;
|
|
if (!form)
|
|
return;
|
|
|
|
form.setFieldValue("amount", availableBalance.value || "0");
|
|
}
|
|
|
|
async function syncAvailableBalance() {
|
|
const values = formRef.value?.getValues();
|
|
if (!values || !values.assetCode || !values.fromAccount)
|
|
return;
|
|
const { data } = await safeClient(client.api.wallet.balance({ assetCode: values.assetCode }).get({
|
|
query: { accountType: values.fromAccount },
|
|
}));
|
|
if (data.value) {
|
|
availableBalance.value = data.value.available;
|
|
}
|
|
}
|
|
|
|
// 提交划转
|
|
async function onSubmit(values: GenericObject) {
|
|
const loading = await loadingController.create({
|
|
message: t("transfer.submitting"),
|
|
});
|
|
await loading.present();
|
|
|
|
const transferData = values as TransferForm;
|
|
const { error } = await safeClient(() => client.api.account_transfer.post({
|
|
assetCode: transferData.assetCode,
|
|
amount: String(transferData.amount),
|
|
fromAccountType: transferData.fromAccount,
|
|
toAccountType: transferData.toAccount,
|
|
}));
|
|
|
|
await loading.dismiss();
|
|
|
|
if (!error.value) {
|
|
const toast = await toastController.create({
|
|
message: t("transfer.successMessage"),
|
|
duration: 2000,
|
|
position: "bottom",
|
|
color: "success",
|
|
});
|
|
|
|
await toast.present();
|
|
|
|
// 刷新余额
|
|
walletStore.syncBalances();
|
|
|
|
router.back();
|
|
}
|
|
}
|
|
|
|
// 账户类型显示名称
|
|
function getAccountTypeName(type: AccountType) {
|
|
return type === "funding" ? t("transfer.fundingAccount") : t("transfer.tradingAccount");
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<ion-page>
|
|
<ion-header>
|
|
<ion-toolbar class="ion-toolbar">
|
|
<ion-buttons slot="start">
|
|
<ui-back-button default-href="/wallet/index" />
|
|
</ion-buttons>
|
|
<ion-title>{{ t("transfer.title") }}</ion-title>
|
|
</ion-toolbar>
|
|
</ion-header>
|
|
|
|
<ion-content :fullscreen="true" class="ion-padding">
|
|
<Form
|
|
ref="formRef"
|
|
:validation-schema="schema"
|
|
:initial-values="initialValues"
|
|
@submit="onSubmit"
|
|
>
|
|
<div class="flex flex-col gap-5">
|
|
<!-- 币种选择 -->
|
|
<div>
|
|
<Field name="assetCode">
|
|
<template #default="{ value }">
|
|
<ion-label class="block text-sm font-medium mb-2">
|
|
{{ t('wallet.transfer.selectCurrency') }}
|
|
</ion-label>
|
|
<div
|
|
class="flex items-center justify-between bg-faint rounded-2xl p-4 cursor-pointer"
|
|
@click="openSelectCurrency"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<component :is="getCryptoIcon(value)" class="w-8 h-8" />
|
|
<span class="text-base font-medium">{{ value }}</span>
|
|
</div>
|
|
<ion-icon name="chevron-forward-outline" class="text-text-400" />
|
|
</div>
|
|
</template>
|
|
</Field>
|
|
<ErrorMessage name="assetCode" class="text-red-500 text-xs mt-1" />
|
|
</div>
|
|
|
|
<!-- 划转账户 -->
|
|
<div class="relative flex flex-col gap-3">
|
|
<Field name="fromAccount">
|
|
<template #default="{ value }">
|
|
<div class="flex flex-col bg-faint rounded-2xl p-3">
|
|
<span class="text-xs text-text-500">{{ t('transfer.from') }}</span>
|
|
<div class="pt-2">
|
|
<span class="text-base font-medium">{{ getAccountTypeName(value) }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Field>
|
|
|
|
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10">
|
|
<ion-button
|
|
fill="solid"
|
|
color="primary"
|
|
class="swap-btn"
|
|
@click="swapAccounts"
|
|
>
|
|
<ion-icon slot="icon-only" :icon="swapVerticalOutline" class="text-xl" />
|
|
</ion-button>
|
|
</div>
|
|
|
|
<Field name="toAccount">
|
|
<template #default="{ value }">
|
|
<div class="flex flex-col bg-faint rounded-2xl p-3">
|
|
<span class="text-xs text-text-500">{{ t('transfer.to') }}</span>
|
|
<div class="pt-2">
|
|
<span class="text-base font-medium">{{ getAccountTypeName(value) }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Field>
|
|
</div>
|
|
|
|
<!-- 划转数量 -->
|
|
<div>
|
|
<Field name="amount">
|
|
<template #default="{ field }">
|
|
<div class="relative">
|
|
<ui-input-label
|
|
v-bind="field"
|
|
:label="t('transfer.amount')"
|
|
:placeholder="t('transfer.enterAmount')"
|
|
type="number"
|
|
inputmode="decimal"
|
|
/>
|
|
<ion-button
|
|
fill="clear"
|
|
size="small"
|
|
class="absolute right-0 top-10.5 text-sm font-semibold z-10"
|
|
@click="setMaxAmount"
|
|
>
|
|
{{ t('wallet.transfer.all') }}
|
|
</ion-button>
|
|
</div>
|
|
</template>
|
|
</Field>
|
|
<ErrorMessage name="amount" class="text-red-500 text-xs mt-1" />
|
|
</div>
|
|
|
|
<!-- 可用余额 -->
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-(--ion-color-medium)">{{ t("transfer.available") }}</span>
|
|
<span class="text-sm font-medium">{{ Number(availableBalance).toFixed(2) }}</span>
|
|
</div>
|
|
|
|
<!-- 提交按钮 -->
|
|
<ion-button
|
|
expand="block"
|
|
type="submit"
|
|
shape="round"
|
|
class="mt-4 h-12 font-semibold"
|
|
>
|
|
{{ t("transfer.submit") }}
|
|
</ion-button>
|
|
</div>
|
|
</Form>
|
|
</ion-content>
|
|
</ion-page>
|
|
</template>
|
|
|
|
<style lang="css" scoped>
|
|
.swap-btn {
|
|
--border-radius: 100%;
|
|
}
|
|
</style>
|