feat: 添加划转功能,更新相关路由和国际化支持

This commit is contained in:
2025-12-24 18:39:49 +07:00
parent a3390f82b5
commit 6f43841b6e
8 changed files with 362 additions and 28 deletions

View File

@@ -365,5 +365,26 @@
"themeLight": "Light",
"themeDark": "Dark",
"themeAuto": "Auto"
},
"transfer": {
"title": "Transfer",
"chooseCurrency": "Choose Currency",
"from": "From",
"to": "To",
"fundingAccount": "Funding Account",
"tradingAccount": "Trading Account",
"available": "Available",
"amount": "Amount",
"enterAmount": "Enter amount",
"all": "All",
"submit": "Confirm Transfer",
"submitting": "Transferring...",
"successMessage": "Transfer successful!",
"assetCodeRequired": "Please select a currency",
"amountRequired": "Please enter amount",
"amountMinError": "Amount must be greater than 0",
"amountMaxError": "Amount cannot exceed available balance {amount}",
"fromAccountRequired": "Please select from account",
"toAccountRequired": "Please select to account"
}
}

View File

@@ -371,5 +371,26 @@
"themeLight": "浅色",
"themeDark": "深色",
"themeAuto": "跟随系统"
},
"transfer": {
"title": "划转",
"chooseCurrency": "选择币种",
"from": "从",
"to": "到",
"fundingAccount": "资金账户",
"tradingAccount": "交易账户",
"available": "可用",
"amount": "划转数量",
"enterAmount": "请输入划转数量",
"all": "全部",
"submit": "确认划转",
"submitting": "划转中...",
"successMessage": "划转成功!",
"assetCodeRequired": "请选择币种",
"amountRequired": "请输入划转数量",
"amountMinError": "划转数量必须大于0",
"amountMaxError": "划转数量不能超过可用余额 {amount}",
"fromAccountRequired": "请选择转出账户",
"toAccountRequired": "请选择转入账户"
}
}

View File

@@ -60,6 +60,11 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("@/views/wallet/bill.vue"),
meta: { requiresAuth: true },
},
{
path: "/wallet/transfer",
component: () => import("@/views/wallet/transfer.vue"),
meta: { requiresAuth: true },
},
{
path: "/user/settings",
component: () => import("@/views/user-settings/outlet.vue"),

View File

@@ -1,12 +1,12 @@
<script lang='ts' setup>
import { eyeOffOutline, eyeOutline } from "ionicons/icons";
import BxTransferAlt from "~icons/bx/transfer-alt";
import IcBaselineBlurCircular from "~icons/ic/baseline-blur-circular";
import IcRoundArrowCircleDown from "~icons/ic/round-arrow-circle-down";
import IcRoundArrowCircleUp from "~icons/ic/round-arrow-circle-up";
import RechargeChannel from "./recharge-channel.vue";
const { t } = useI18n();
const router = useRouter();
const walletStore = useWalletStore();
const { totalAssetValue } = storeToRefs(walletStore);
const rechargeInstance = ref<ModalInstance>();
@@ -16,12 +16,6 @@ const totalAsset = computed(() => Number(totalAssetValue.value.totalValueUsd).to
function onCloseModal() {
rechargeInstance.value?.$el.dismiss(null, "confirm");
}
function handleWithdraw() {
router.push("/withdraw/index");
}
function handleBill() {
router.push("/wallet/bill");
}
onMounted(() => {
walletStore.syncFundingBalances();
@@ -45,21 +39,42 @@ onMounted(() => {
</div>
</div>
<div class="flex gap-5 w-full">
<ion-button id="open-recharge-modal" expand="full" color="success" shape="round" class="w-full min-h-10 h-10">
<IcRoundArrowCircleDown slot="start" />
{{ t("wallet.recharge") }}
</ion-button>
<div class="flex gap-2 w-full justify-around">
<div id="open-recharge-modal" class="flex-col-center">
<ion-button expand="full" color="success" shape="round" class="w-12 h-12">
<IcRoundArrowCircleDown slot="icon-only" />
</ion-button>
<div class="text-sm font-medium mt-1">
{{ t("wallet.recharge") }}
</div>
</div>
<ion-button expand="full" color="success" shape="round" class="w-full min-h-10 h-10" @click="handleWithdraw">
<IcRoundArrowCircleUp slot="start" />
{{ t("wallet.withdraw") }}
</ion-button>
<div class="flex-col-center" @click="$router.push('/withdraw/index')">
<ion-button expand="full" color="success" shape="round" class="w-12 h-12">
<IcRoundArrowCircleUp slot="icon-only" />
</ion-button>
<div class="text-sm font-medium mt-1">
{{ t("wallet.withdraw") }}
</div>
</div>
<ion-button expand="full" color="success" shape="round" class="w-full min-h-10 h-10" @click="handleBill">
<IcBaselineBlurCircular slot="start" />
账单
</ion-button>
<div class="flex-col-center" @click="$router.push('/wallet/transfer')">
<ion-button expand="full" color="success" shape="round" class="w-12 h-12">
<BxTransferAlt slot="icon-only" />
</ion-button>
<div class="text-sm font-medium mt-1">
划转
</div>
</div>
<div class="flex-col-center" @click="$router.push('/wallet/bill')">
<ion-button expand="full" color="success" shape="round" class="w-12 h-12">
<IcBaselineBlurCircular slot="icon-only" />
</ion-button>
<div class="text-sm font-medium mt-1">
账单
</div>
</div>
<!-- <div id="open-recharge-modal" class="flex-col-center">
<ion-ripple-effect />
<ion-button shape="round" color="success" size="large">

View File

@@ -0,0 +1,261 @@
<script lang='ts' setup>
import type { GenericObject } from "vee-validate";
import type { FormInstance } from "@/utils";
import { loadingController, toastController } from "@ionic/vue";
import { swapVerticalOutline } from "ionicons/icons";
import { ErrorMessage, Field, Form } from "vee-validate";
import * as yup from "yup";
import { client, safeClient } from "@/api";
import { AssetCodeEnum } from "@/api/enum";
const { t } = useI18n();
const router = useRouter();
const walletStore = useWalletStore();
const { fundingBalances, tradingBalances } = storeToRefs(walletStore);
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 = computed(() => {
const form = formRef.value?.values as TransferForm | undefined;
if (!form)
return "0";
const balances = form.fromAccount === "funding" ? fundingBalances.value : tradingBalances.value;
const balance = balances?.find(item => item.assetCode === form.assetCode);
return balance ? balance.available : "0";
});
// 验证规则
const schema = computed(() => yup.object({
assetCode: yup.string().required(t("transfer.assetCodeRequired")),
amount: yup
.string()
.required(t("transfer.amountRequired"))
.test("min", t("transfer.amountMinError"), value => Number(value) > 0)
.test("max", t("transfer.amountMaxError", { amount: availableBalance.value }), value => Number(value) <= Number(availableBalance.value)),
fromAccount: yup.string().required(t("transfer.fromAccountRequired")),
toAccount: yup.string().required(t("transfer.toAccountRequired")),
}));
// 交换账户
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);
}
// 设置全部金额
function setMaxAmount() {
const form = formRef.value as any;
if (!form)
return;
form.setFieldValue("amount", availableBalance.value);
}
// 提交划转
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.syncFundingBalances();
walletStore.syncTradingBalances();
router.back();
}
}
// 账户类型显示名称
function getAccountTypeName(type: AccountType) {
return type === "funding" ? t("transfer.fundingAccount") : t("transfer.tradingAccount");
}
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar class="ui-toolbar">
<ion-buttons slot="start">
<ion-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="{ field, value }">
<ion-radio-group v-bind="field" :model-value="value">
<ion-label class="block text-sm font-medium mb-3">
{{ t("transfer.chooseCurrency") }}
</ion-label>
<div class="flex gap-3">
<ion-item
v-for="item in AssetCodeEnum"
:key="item"
class="flex-1"
lines="none"
>
<ion-radio :value="item">
{{ item }}
</ion-radio>
</ion-item>
</div>
</ion-radio-group>
</template>
</Field>
<ErrorMessage name="assetCode" class="text-red-500 text-xs mt-1" />
</div>
<!-- 划转账户 -->
<div class="relative flex flex-col">
<Field name="fromAccount">
<template #default="{ field, value }">
<ion-select
v-bind="field"
:model-value="value"
class="ui-select"
interface="action-sheet"
toggle-icon=""
:label="t('transfer.from')"
>
<ion-select-option value="funding">
{{ getAccountTypeName("funding") }}
</ion-select-option>
<ion-select-option value="trading">
{{ getAccountTypeName("trading") }}
</ion-select-option>
</ion-select>
</template>
</Field>
<div class="absolute left-7.5 top-1/2 -translate-y-1/2 z-10">
<ion-button
color="primary"
@click="swapAccounts"
>
<ion-icon slot="icon-only" :icon="swapVerticalOutline" />
</ion-button>
</div>
<Field name="toAccount">
<template #default="{ field, value }">
<ion-select
v-bind="field"
:model-value="value"
class="ui-select"
interface="action-sheet"
toggle-icon=""
:label="t('transfer.to')"
>
<ion-select-option value="funding">
{{ getAccountTypeName("funding") }}
</ion-select-option>
<ion-select-option value="trading">
{{ getAccountTypeName("trading") }}
</ion-select-option>
</ion-select>
</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-8 text-sm font-semibold"
@click="setMaxAmount"
>
{{ t("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>