feat: 添加交易相关组件,包括订单簿、交易表单和交易对头部信息,优化交易界面
This commit is contained in:
158
src/views/trade/components/order-book.vue
Normal file
158
src/views/trade/components/order-book.vue
Normal 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>
|
||||
333
src/views/trade/components/orders-panel.vue
Normal file
333
src/views/trade/components/orders-panel.vue
Normal 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>
|
||||
292
src/views/trade/components/trade-form.vue
Normal file
292
src/views/trade/components/trade-form.vue
Normal 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>
|
||||
90
src/views/trade/components/trading-pair-header.vue
Normal file
90
src/views/trade/components/trading-pair-header.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
10
vault.md
10
vault.md
@@ -1,8 +1,8 @@
|
||||
open float Y 开盘价 每一个纬度(例如天纬度,就是一天的第一笔成交价格)的第一笔成交价格
|
||||
high float Y 最高价
|
||||
low float Y 最低价
|
||||
close float Y 收盘价
|
||||
settle float Y 结算价 每一个纬度(某一个时间段之内的金额总和 / 交易量)
|
||||
open float Y 开盘价 每一个纬度(例如天纬度,就是一天的第一笔成交价格)的第一笔成交价格
|
||||
high float Y 最高价
|
||||
low float Y 最低价
|
||||
close float Y 收盘价
|
||||
settle float Y 结算价 每一个纬度(某一个时间段之内的金额总和 / 交易量)
|
||||
|
||||
接口
|
||||
代币化分类接口
|
||||
|
||||
Reference in New Issue
Block a user