feat: 添加交易相关组件,包括订单簿、交易表单和交易对头部信息,优化交易界面

This commit is contained in:
2025-12-27 21:51:02 +07:00
parent 13089ca2c6
commit 0b0e67114c
7 changed files with 983 additions and 17 deletions

View File

@@ -0,0 +1,158 @@
<script setup lang="ts">
interface OrderBookItem {
price: string;
amount: string;
total: string;
}
interface OrderBookData {
bids: OrderBookItem[]; // 买单
asks: OrderBookItem[]; // 卖单
}
// TODO: 后续从API获取实时数据
// const { data } = await client.api.trading.orderbook.get({ query: { symbol: 'BTC_USDT', depth: 20 } })
const orderBook = ref<OrderBookData>({
asks: [
{ price: "43260.50", amount: "0.125", total: "5.407" },
{ price: "43258.00", amount: "0.342", total: "14.792" },
{ price: "43255.80", amount: "0.856", total: "37.027" },
{ price: "43253.20", amount: "1.234", total: "53.350" },
{ price: "43251.00", amount: "0.678", total: "29.324" },
{ price: "43249.50", amount: "2.145", total: "92.740" },
{ price: "43247.00", amount: "0.456", total: "19.720" },
{ price: "43245.80", amount: "1.567", total: "67.768" },
],
bids: [
{ price: "43250.50", amount: "0.234", total: "10.120" },
{ price: "43248.00", amount: "0.567", total: "24.522" },
{ price: "43245.50", amount: "1.234", total: "53.365" },
{ price: "43243.20", amount: "0.789", total: "34.118" },
{ price: "43240.00", amount: "2.456", total: "106.197" },
{ price: "43238.50", amount: "0.345", total: "14.917" },
{ price: "43235.00", amount: "1.678", total: "72.548" },
{ price: "43232.80", amount: "0.923", total: "39.903" },
],
});
// 计算买卖盘深度百分比(用于显示背景条)
function getDepthPercent(items: OrderBookItem[], index: number) {
const maxTotal = Math.max(...items.map(item => Number.parseFloat(item.total)));
const currentTotal = Number.parseFloat(items[index].total);
return (currentTotal / maxTotal) * 100;
}
</script>
<template>
<div class="order-book">
<!-- 卖单 (asks) - 从下往上排列 -->
<div class="asks-section">
<div
v-for="(ask, index) in orderBook.asks.slice().reverse()"
:key="`ask-${index}`"
class="order-item ask-item"
>
<div
class="depth-bg ask-depth"
:style="{ width: `${getDepthPercent(orderBook.asks, orderBook.asks.length - 1 - index)}%` }"
/>
<div class="order-content">
<span class="price text-danger-500">{{ ask.price }}</span>
<span class="amount">{{ ask.amount }}</span>
<span class="total">{{ ask.total }}</span>
</div>
</div>
</div>
<!-- 中间分隔 - 显示最新成交价 -->
<div class="spread-section">
<div class="flex items-center justify-between px-3 py-2">
<div class="text-lg font-semibold text-success-500">
43250.50
</div>
<div class="text-xs text-text-500">
$43,250.50
</div>
</div>
</div>
<!-- 买单 (bids) -->
<div class="bids-section">
<div
v-for="(bid, index) in orderBook.bids"
:key="`bid-${index}`"
class="order-item bid-item"
>
<div
class="depth-bg bid-depth"
:style="{ width: `${getDepthPercent(orderBook.bids, index)}%` }"
/>
<div class="order-content">
<span class="price text-success-500">{{ bid.price }}</span>
<span class="amount">{{ bid.amount }}</span>
<span class="total">{{ bid.total }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.order-book {
font-size: 12px;
user-select: none;
}
.asks-section,
.bids-section {
position: relative;
}
.order-item {
position: relative;
height: 20px;
display: flex;
align-items: center;
}
.depth-bg {
position: absolute;
top: 0;
right: 0;
height: 100%;
transition: width 0.3s ease;
}
.ask-depth {
background: rgba(239, 68, 68, 0.1);
}
.bid-depth {
background: rgba(34, 197, 94, 0.1);
}
.order-content {
position: relative;
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
padding: 0 12px;
z-index: 1;
}
.price {
font-weight: 500;
}
.amount,
.total {
color: var(--ion-text-color);
text-align: right;
}
.spread-section {
border-top: 1px solid var(--ion-border-color);
border-bottom: 1px solid var(--ion-border-color);
background: var(--ion-background-color);
}
</style>

View File

@@ -0,0 +1,333 @@
<script setup lang="ts">
interface Order {
id: string;
symbol: string;
side: "buy" | "sell";
type: "limit" | "market";
price: string;
amount: string;
filled: string;
total: string;
status: "pending" | "partial" | "filled" | "cancelled";
time: string;
}
type TabType = "current" | "history";
const activeTab = ref<TabType>("current");
// TODO: 后续从API获取当前委托订单
// const { data: currentOrders } = await client.api.trading.orders.get({ query: { status: 'open' } })
const currentOrders = ref<Order[]>([
{
id: "1001",
symbol: "BTC_USDT",
side: "buy",
type: "limit",
price: "43200.00",
amount: "0.5000",
filled: "0.2500",
total: "21600.00",
status: "partial",
time: "2024-12-27 10:30:25",
},
{
id: "1002",
symbol: "BTC_USDT",
side: "sell",
type: "limit",
price: "43500.00",
amount: "0.3000",
filled: "0.0000",
total: "13050.00",
status: "pending",
time: "2024-12-27 09:15:10",
},
]);
// TODO: 后续从API获取历史订单
// const { data: historyOrders } = await client.api.trading.orders.get({ query: { status: 'closed' } })
const historyOrders = ref<Order[]>([
{
id: "1003",
symbol: "BTC_USDT",
side: "buy",
type: "limit",
price: "42800.00",
amount: "0.2000",
filled: "0.2000",
total: "8560.00",
status: "filled",
time: "2024-12-26 18:45:30",
},
{
id: "1004",
symbol: "BTC_USDT",
side: "sell",
type: "market",
price: "43100.00",
amount: "0.1500",
filled: "0.1500",
total: "6465.00",
status: "filled",
time: "2024-12-26 16:20:15",
},
{
id: "1005",
symbol: "BTC_USDT",
side: "buy",
type: "limit",
price: "42500.00",
amount: "0.4000",
filled: "0.0000",
total: "17000.00",
status: "cancelled",
time: "2024-12-26 14:10:05",
},
]);
const displayOrders = computed(() => {
return activeTab.value === "current" ? currentOrders.value : historyOrders.value;
});
function getStatusText(status: Order["status"]) {
const statusMap = {
pending: "未成交",
partial: "部分成交",
filled: "已成交",
cancelled: "已取消",
};
return statusMap[status];
}
function getStatusColor(status: Order["status"]) {
const colorMap = {
pending: "warning",
partial: "primary",
filled: "success",
cancelled: "medium",
};
return colorMap[status];
}
async function cancelOrder(orderId: string) {
// TODO: 后续调用取消订单API
// await client.api.trading.order[orderId].delete()
console.log("取消订单", orderId);
}
</script>
<template>
<div class="orders-panel">
<!-- Tab切换 -->
<div class="tabs-header">
<button
:class="{ active: activeTab === 'current' }"
@click="activeTab = 'current'"
>
当前委托 ({{ currentOrders.length }})
</button>
<button
:class="{ active: activeTab === 'history' }"
@click="activeTab = 'history'"
>
历史记录
</button>
</div>
<!-- 订单列表 -->
<div class="orders-list">
<div v-if="displayOrders.length === 0" class="empty-state">
<div class="text-text-500 text-sm">
{{ activeTab === 'current' ? '暂无当前委托' : '暂无历史记录' }}
</div>
</div>
<div v-else class="orders-content">
<div
v-for="order in displayOrders"
:key="order.id"
class="order-item"
>
<div class="order-header">
<div class="flex items-center gap-2">
<span
class="side-badge"
:class="order.side === 'buy' ? 'buy' : 'sell'"
>
{{ order.side === 'buy' ? '买入' : '卖出' }}
</span>
<span class="text-sm font-medium">{{ order.symbol }}</span>
<ion-badge :color="getStatusColor(order.status)" class="status-badge">
{{ getStatusText(order.status) }}
</ion-badge>
</div>
<button
v-if="activeTab === 'current' && order.status !== 'filled'"
class="cancel-btn"
@click="cancelOrder(order.id)"
>
撤单
</button>
</div>
<div class="order-details">
<div class="detail-row">
<span class="label">{{ order.type === 'limit' ? '价格' : '市价' }}</span>
<span class="value">{{ order.type === 'limit' ? order.price : '-' }}</span>
</div>
<div class="detail-row">
<span class="label">数量</span>
<span class="value">{{ order.amount }}</span>
</div>
<div class="detail-row">
<span class="label">成交</span>
<span class="value">{{ order.filled }}</span>
</div>
<div class="detail-row">
<span class="label">总额</span>
<span class="value">{{ order.total }} USDT</span>
</div>
</div>
<div class="order-footer">
<span class="time">{{ order.time }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.orders-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--ion-background-color);
}
.tabs-header {
display: flex;
border-bottom: 1px solid var(--ion-border-color);
background: var(--ion-background-color);
}
.tabs-header button {
flex: 1;
padding: 12px 0;
background: transparent;
border: none;
color: var(--ion-text-color-step-400);
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.tabs-header button.active {
color: var(--ion-color-primary);
border-bottom-color: var(--ion-color-primary);
}
.orders-list {
flex: 1;
overflow-y: auto;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.orders-content {
padding: 12px;
}
.order-item {
background: var(--ion-color-light);
border-radius: 12px;
padding: 12px;
margin-bottom: 12px;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.side-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.side-badge.buy {
background: rgba(34, 197, 94, 0.1);
color: var(--ion-color-success);
}
.side-badge.sell {
background: rgba(239, 68, 68, 0.1);
color: var(--ion-color-danger);
}
.status-badge {
font-size: 10px;
padding: 2px 6px;
}
.cancel-btn {
padding: 4px 12px;
background: transparent;
border: 1px solid var(--ion-color-danger);
color: var(--ion-color-danger);
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.cancel-btn:active {
background: var(--ion-color-danger);
color: white;
}
.order-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 8px;
}
.detail-row {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.detail-row .label {
color: var(--ion-text-color-step-400);
}
.detail-row .value {
color: var(--ion-text-color);
font-weight: 500;
}
.order-footer {
padding-top: 8px;
border-top: 1px solid var(--ion-border-color);
}
.order-footer .time {
font-size: 11px;
color: var(--ion-text-color-step-500);
}
</style>

View File

@@ -0,0 +1,292 @@
<script setup lang="ts">
type OrderType = "limit" | "market";
type TradeAction = "buy" | "sell";
interface TradeFormData {
orderType: OrderType;
price: string;
amount: string;
total: string;
}
const props = defineProps<{
action: TradeAction;
}>();
const formData = reactive<TradeFormData>({
orderType: "limit",
price: "",
amount: "",
total: "",
});
// TODO: 后续从API获取用户余额
// const { data: balance } = await client.api.wallet.balances.get({ query: { accountType: 'trading' } })
const availableBalance = ref({
base: "0.00000000", // BTC
quote: "10000.00", // USDT
});
const isBuy = computed(() => props.action === "buy");
const buttonColor = computed(() => isBuy.value ? "success" : "danger");
const buttonText = computed(() => isBuy.value ? "买入 BTC" : "卖出 BTC");
// 计算总价
watch(() => [formData.price, formData.amount], () => {
if (formData.price && formData.amount) {
const price = Number.parseFloat(formData.price);
const amount = Number.parseFloat(formData.amount);
if (!Number.isNaN(price) && !Number.isNaN(amount)) {
formData.total = (price * amount).toFixed(2);
}
}
});
// 根据总价计算数量
watch(() => formData.total, (newTotal) => {
if (newTotal && formData.price) {
const total = Number.parseFloat(newTotal);
const price = Number.parseFloat(formData.price);
if (!Number.isNaN(total) && !Number.isNaN(price) && price > 0) {
formData.amount = (total / price).toFixed(8);
}
}
});
function setPercentage(percent: number) {
if (isBuy.value) {
// 买入根据USDT余额计算
const balance = Number.parseFloat(availableBalance.value.quote);
const total = balance * percent;
formData.total = total.toFixed(2);
}
else {
// 卖出根据BTC余额计算
const balance = Number.parseFloat(availableBalance.value.base);
formData.amount = (balance * percent).toFixed(8);
}
}
async function handleSubmit() {
// TODO: 后续调用交易API
// await client.api.trading.order.post({
// symbol: 'BTC_USDT',
// side: props.action,
// type: formData.orderType,
// price: formData.orderType === 'limit' ? formData.price : undefined,
// quantity: formData.amount
// })
console.log("提交订单", {
action: props.action,
...formData,
});
}
</script>
<template>
<div class="trade-form">
<!-- 订单类型切换 -->
<div class="order-type-tabs">
<button
:class="{ active: formData.orderType === 'limit' }"
@click="formData.orderType = 'limit'"
>
限价
</button>
<button
:class="{ active: formData.orderType === 'market' }"
@click="formData.orderType = 'market'"
>
市价
</button>
</div>
<!-- 可用余额 -->
<div class="balance-info">
<span class="label">可用</span>
<span class="value">
{{ isBuy ? availableBalance.quote : availableBalance.base }}
{{ isBuy ? 'USDT' : 'BTC' }}
</span>
</div>
<!-- 价格输入 -->
<div v-if="formData.orderType === 'limit'" class="input-group">
<label>价格</label>
<div class="input-wrapper">
<input
v-model="formData.price"
type="number"
placeholder="请输入价格"
>
<span class="unit">USDT</span>
</div>
</div>
<!-- 数量输入 -->
<div class="input-group">
<label>数量</label>
<div class="input-wrapper">
<input
v-model="formData.amount"
type="number"
placeholder="请输入数量"
>
<span class="unit">BTC</span>
</div>
</div>
<!-- 百分比选择 -->
<div class="percentage-buttons">
<button @click="setPercentage(0.25)">
25%
</button>
<button @click="setPercentage(0.5)">
50%
</button>
<button @click="setPercentage(0.75)">
75%
</button>
<button @click="setPercentage(1)">
100%
</button>
</div>
<!-- 总额 -->
<div class="input-group">
<label>总额</label>
<div class="input-wrapper">
<input
v-model="formData.total"
type="number"
placeholder="请输入总额"
>
<span class="unit">USDT</span>
</div>
</div>
<!-- 提交按钮 -->
<ion-button
expand="block"
:color="buttonColor"
class="submit-button"
@click="handleSubmit"
>
{{ buttonText }}
</ion-button>
</div>
</template>
<style scoped>
.trade-form {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.order-type-tabs {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.order-type-tabs button {
flex: 1;
padding: 8px 0;
background: transparent;
border: none;
color: var(--ion-text-color-step-400);
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.order-type-tabs button.active {
color: var(--ion-color-primary);
border-bottom-color: var(--ion-color-primary);
}
.balance-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--ion-text-color-step-400);
margin-bottom: 4px;
}
.balance-info .value {
color: var(--ion-text-color);
font-weight: 500;
}
.input-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-group label {
font-size: 12px;
color: var(--ion-text-color-step-400);
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
background: var(--ion-color-light);
border-radius: 8px;
padding: 12px;
}
.input-wrapper input {
flex: 1;
background: transparent;
border: none;
outline: none;
font-size: 14px;
color: var(--ion-text-color);
}
.input-wrapper input::placeholder {
color: var(--ion-text-color-step-600);
}
.input-wrapper .unit {
font-size: 12px;
color: var(--ion-text-color-step-400);
margin-left: 8px;
}
.percentage-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.percentage-buttons button {
padding: 6px 0;
background: var(--ion-color-light);
border: none;
border-radius: 6px;
font-size: 12px;
color: var(--ion-text-color);
cursor: pointer;
transition: all 0.2s;
}
.percentage-buttons button:active {
transform: scale(0.95);
background: var(--ion-color-light-shade);
}
.submit-button {
margin-top: 8px;
--border-radius: 8px;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import { chevronDownOutline } from "ionicons/icons";
interface TradingPairData {
symbol: string;
baseAsset: string;
quoteAsset: string;
lastPrice: string;
priceChange24h: string;
priceChangePercent24h: string;
high24h: string;
low24h: string;
volume24h: string;
}
// TODO: 后续从API获取数据
// const { data } = await client.api.trading.pair.get({ query: { symbol: 'BTC_USDT' } })
const tradingPair = ref<TradingPairData>({
symbol: "BTC_USDT",
baseAsset: "BTC",
quoteAsset: "USDT",
lastPrice: "43250.50",
priceChange24h: "+1250.30",
priceChangePercent24h: "+2.98",
high24h: "43800.00",
low24h: "41500.00",
volume24h: "1,234.56",
});
const isPositive = computed(() => !tradingPair.value.priceChangePercent24h.startsWith("-"));
function openPairSelector() {
// TODO: 打开交易对选择器
console.log("打开交易对选择器");
}
</script>
<template>
<div class="trading-pair-header">
<div class="flex items-center justify-between px-4 py-3">
<!-- 交易对信息 -->
<div class="flex items-center gap-2" @click="openPairSelector">
<div class="flex flex-col">
<div class="flex items-center gap-1">
<span class="text-base font-semibold">{{ tradingPair.baseAsset }}/{{ tradingPair.quoteAsset }}</span>
<ion-icon :icon="chevronDownOutline" class="text-sm text-text-400" />
</div>
<div class="text-xs text-text-500">
现货
</div>
</div>
</div>
<!-- 价格信息 -->
<div class="flex-1 flex justify-end items-center gap-4">
<div class="flex flex-col items-end">
<div class="text-lg font-semibold" :class="isPositive ? 'text-success-500' : 'text-danger-500'">
{{ tradingPair.lastPrice }}
</div>
<div class="text-xs" :class="isPositive ? 'text-success-500' : 'text-danger-500'">
{{ tradingPair.priceChangePercent24h }}%
</div>
</div>
</div>
</div>
<!-- 24h数据 -->
<div class="grid grid-cols-3 gap-4 px-4 pb-3 text-xs">
<div class="flex flex-col">
<span class="text-text-500">24h高</span>
<span class="font-medium mt-1">{{ tradingPair.high24h }}</span>
</div>
<div class="flex flex-col">
<span class="text-text-500">24h低</span>
<span class="font-medium mt-1">{{ tradingPair.low24h }}</span>
</div>
<div class="flex flex-col">
<span class="text-text-500">24h量({{ tradingPair.baseAsset }})</span>
<span class="font-medium mt-1">{{ tradingPair.volume24h }}</span>
</div>
</div>
</div>
</template>
<style scoped>
.trading-pair-header {
background: var(--ion-background-color);
border-bottom: 1px solid var(--ion-border-color);
}
</style>

View File

@@ -1,34 +1,127 @@
<script setup lang="ts">
const { t } = useI18n();
import OrderBook from "./components/order-book.vue";
import OrdersPanel from "./components/orders-panel.vue";
import TradeForm from "./components/trade-form.vue";
import TradingPairHeader from "./components/trading-pair-header.vue";
const tradingViewContainer = useTemplateRef<HTMLElement>("tradingViewContainer");
// K线图配置
useTradingView(tradingViewContainer, {
data: [
// 随机k线图数据
{ time: "2023-10-01", open: 100, high: 110, low: 90, close: 105 },
{ time: "2023-10-02", open: 105, high: 115, low: 95, close: 100 },
{ time: "2023-10-03", open: 100, high: 120, low: 80, close: 110 },
{ time: "2023-10-04", open: 110, high: 130, low: 100, close: 120 },
{ time: "2023-10-05", open: 120, high: 140, low: 110, close: 130 },
{ time: "2023-10-06", open: 130, high: 150, low: 120, close: 140 },
{ time: "2023-10-07", open: 140, high: 160, low: 130, close: 150 },
// TODO: 后续从API获取K线数据
// const { data } = await client.api.trading.kline.get({ query: { symbol: 'BTC_USDT', interval: '1h' } })
{ time: "2023-10-01", open: 42000, high: 43100, low: 41800, close: 42500 },
{ time: "2023-10-02", open: 42500, high: 43500, low: 42200, close: 43000 },
{ time: "2023-10-03", open: 43000, high: 44200, low: 42800, close: 43800 },
{ time: "2023-10-04", open: 43800, high: 44500, low: 43500, close: 44200 },
{ time: "2023-10-05", open: 44200, high: 44800, low: 43900, close: 44500 },
{ time: "2023-10-06", open: 44500, high: 45000, low: 44200, close: 44800 },
{ time: "2023-10-07", open: 44800, high: 45500, low: 44500, close: 43250 },
],
});
// 买卖切换
const tradeAction = ref<"buy" | "sell">("buy");
</script>
<template>
<ion-page>
<ion-header class="ion-no-border">
<ion-toolbar class="ui-toolbar">
<ion-title>{{ t('tabs.trade') }}</ion-title>
<ion-title>交易</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<div ref="tradingViewContainer" />
<!-- 交易对头部信息 -->
<TradingPairHeader />
<!-- K线图表 -->
<div ref="tradingViewContainer" class="chart-container" />
<!-- 订单簿 -->
<div class="order-book-section">
<OrderBook />
</div>
<!-- 买卖切换 -->
<div class="trade-action-tabs">
<button
:class="{ active: tradeAction === 'buy' }"
class="buy-tab"
@click="tradeAction = 'buy'"
>
买入
</button>
<button
:class="{ active: tradeAction === 'sell' }"
class="sell-tab"
@click="tradeAction = 'sell'"
>
卖出
</button>
</div>
<!-- 交易表单 -->
<TradeForm :action="tradeAction" />
<!-- 当前委托和历史记录 -->
<div class="orders-section">
<OrdersPanel />
</div>
</ion-content>
</ion-page>
</template>
<style scoped>
.chart-container {
height: 300px;
width: 100%;
}
.order-book-section {
margin: 16px 0;
}
.trade-action-tabs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
padding: 0 16px;
margin-bottom: 8px;
}
.trade-action-tabs button {
padding: 12px 0;
border: none;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
background: var(--ion-color-light);
color: var(--ion-text-color);
}
.trade-action-tabs .buy-tab {
border-radius: 8px 0 0 8px;
}
.trade-action-tabs .sell-tab {
border-radius: 0 8px 8px 0;
}
.trade-action-tabs button.active.buy-tab {
background: var(--ion-color-success);
color: white;
}
.trade-action-tabs button.active.sell-tab {
background: var(--ion-color-danger);
color: white;
}
.orders-section {
margin-top: 16px;
min-height: 300px;
}
</style>

View File

@@ -10,7 +10,7 @@ export function createWithdrawSchema(t: (key: string, params?: any) => string, m
.string()
.required(t("withdraw.validation.amountRequired"))
.test("is-number", t("withdraw.validation.amountInvalid"), (value) => {
return /^\d+(\.\d+)?$/.test(value || "");
return /^\d+(?:\.\d+)?$/.test(value || "");
})
.test("max-amount", t("withdraw.validation.amountExceedsBalance"), (value) => {
if (!value || maxAmount === "0")