xtraderClient/src/components/OrderBook.vue
2026-02-08 11:41:39 +08:00

487 lines
10 KiB
Vue

<template>
<v-card class="order-book">
<!-- Header -->
<div class="order-book-header">
<h3 class="order-book-title">Order Book</h3>
<div class="order-book-vol">
$4.4k Vol.
<v-icon size="14" class="order-book-icon">mdi-chevron-up</v-icon>
</div>
</div>
<!-- Trade Tabs -->
<div class="trade-tabs-container">
<v-tabs v-model="activeTrade" class="trade-tabs" density="comfortable">
<v-tab value="up" class="trade-tab">Trade Up</v-tab>
<v-tab value="down" class="trade-tab">Trade Down</v-tab>
</v-tabs>
<div class="maker-rebate">
<span class="maker-rebate-text">Maker Rebate</span>
<v-icon size="14">mdi-refresh</v-icon>
</div>
</div>
<!-- Order Book Content -->
<div class="order-book-content">
<!-- Order List -->
<div class="order-list">
<div class="order-list-header">
<div class="order-list-header-price">PRICE</div>
<div class="order-list-header-shares">SHARES</div>
<div class="order-list-header-total">TOTAL</div>
</div>
<!-- Asks Orders -->
<div class="asks-label">Asks</div>
<div v-for="(ask, index) in asksWithCumulativeTotal" :key="index" class="order-item">
<div class="order-progress">
<HorizontalProgressBar
:max="maxAsksTotal"
:value="ask.total"
:fillStyle="{ backgroundColor: '#ffb3ba' }"
:trackStyle="{ backgroundColor: 'transparent' }"
/>
</div>
<div class="order-price asks-price">{{ ask.price }}¢</div>
<div class="order-shares">{{ ask.shares.toFixed(2) }}</div>
<div class="order-total">{{ ask.cumulativeTotal.toFixed(2) }}</div>
</div>
<!-- Bids Orders -->
<div class="bids-label">Bids</div>
<div
v-for="(bid, index) in bidsWithCumulativeTotal"
:key="index"
class="order-item bids-item"
>
<div class="order-progress">
<HorizontalProgressBar
:max="maxBidsTotal"
:value="bid.total"
:fillStyle="{ backgroundColor: '#b3ffb3' }"
:trackStyle="{ backgroundColor: 'transparent' }"
/>
</div>
<div class="order-price bids-price">{{ bid.price }}¢</div>
<div class="order-shares">{{ bid.shares.toFixed(2) }}</div>
<div class="order-total">{{ bid.cumulativeTotal.toFixed(2) }}</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="order-book-footer">
<div class="last-price">Last: {{ lastPrice }}¢</div>
<div class="spread">Spread: {{ spread }}¢</div>
</div>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import HorizontalProgressBar from './HorizontalProgressBar.vue'
// State
const activeTrade = ref('up')
const lastPrice = ref(37)
const spread = ref(1)
// Mock asks data (卖出订单)
const asks = ref([
{ price: 45, shares: 1000.0 },
{ price: 44, shares: 2500.0 },
{ price: 43, shares: 1800.0 },
{ price: 42, shares: 3200.0 },
{ price: 41, shares: 2000.0 },
{ price: 40, shares: 1500.0 },
{ price: 39, shares: 800.0 },
{ price: 38, shares: 500.0 },
{ price: 37, shares: 300.0 },
])
// Mock bids data (买入订单)
const bids = ref([
{ price: 36, shares: 200.0 },
{ price: 35, shares: 500.0 },
{ price: 34, shares: 1000.0 },
{ price: 33, shares: 1500.0 },
{ price: 32, shares: 2000.0 },
{ price: 31, shares: 2500.0 },
{ price: 30, shares: 3000.0 },
{ price: 29, shares: 2800.0 },
{ price: 28, shares: 2500.0 },
{ price: 27, shares: 2000.0 },
{ price: 26, shares: 1500.0 },
{ price: 25, shares: 1000.0 },
])
// Simulate dynamic data updates
setInterval(() => {
// Update random ask price and shares
const randomAskIndex = Math.floor(Math.random() * asks.value.length)
asks.value[randomAskIndex] = {
...asks.value[randomAskIndex],
shares: Math.max(0, asks.value[randomAskIndex].shares + Math.floor(Math.random() * 100) - 50),
}
// Update random bid price and shares
const randomBidIndex = Math.floor(Math.random() * bids.value.length)
bids.value[randomBidIndex] = {
...bids.value[randomBidIndex],
shares: Math.max(0, bids.value[randomBidIndex].shares + Math.floor(Math.random() * 100) - 50),
}
// Update last price
lastPrice.value = lastPrice.value + Math.floor(Math.random() * 3) - 1
// Update spread
spread.value = spread.value + Math.floor(Math.random() * 2) - 1
if (spread.value < 1) spread.value = 1
}, 2000) // Update every 2 seconds
// Calculate cumulative total for asks
const asksWithCumulativeTotal = computed(() => {
let cumulativeTotal = 0
// Sort asks by price in descending order
const sortedAsks = [...asks.value].sort((a, b) => a.price - b.price)
return sortedAsks
.map((ask) => {
// Calculate current ask's value
const askValue = (ask.price * ask.shares) / 100 // Convert cents to dollars
cumulativeTotal += askValue
return {
...ask,
total: cumulativeTotal, // Use calculated total instead of fixed value
cumulativeTotal,
}
})
.sort((a, b) => b.price - a.price)
})
// Calculate cumulative total for bids
const bidsWithCumulativeTotal = computed(() => {
let cumulativeTotal = 0
const sortedBids = [...bids.value].sort((a, b) => b.price - a.price)
return sortedBids.map((bid) => {
// Calculate current bid's value
const bidValue = (bid.price * bid.shares) / 100 // Convert cents to dollars
cumulativeTotal += bidValue
return {
...bid,
total: cumulativeTotal, // Use calculated total instead of fixed value
cumulativeTotal,
}
})
// Reverse to display in descending order
})
// Calculate max total from cumulative totals
const maxAsksTotal = computed(() => {
const askTotals = asksWithCumulativeTotal.value.map((item) => item.cumulativeTotal)
const allTotals = [...askTotals]
return Math.max(...allTotals)
})
const maxBidsTotal = computed(() => {
const bidTotals = bidsWithCumulativeTotal.value.map((item) => item.cumulativeTotal)
const allTotals = [...bidTotals]
return Math.max(...allTotals)
})
</script>
<style scoped>
.order-book {
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.horizontal-progress-bar {
height: 33px !important;
border-radius: 0px !important;
}
.progress-fill {
border-radius: 0px !important;
}
.order-book-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: none;
background-color: transparent;
}
.order-book-title {
font-size: 16px;
font-weight: 500;
margin: 0;
color: #333333;
}
.order-book-vol {
font-size: 14px;
color: #666666;
display: flex;
align-items: center;
gap: 4px;
}
.order-book-icon {
color: #666666;
}
.trade-tabs-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px 12px 16px;
border-bottom: none;
background-color: transparent;
}
.trade-tabs {
border-bottom: none;
background-color: transparent;
}
.trade-tab {
font-size: 14px;
text-transform: none;
color: #666666;
min-width: 120px;
margin-right: 8px;
background-color: transparent;
border: none;
border-radius: 0;
}
.trade-tab.v-tab--active {
color: #0066cc;
font-weight: 500;
background-color: transparent;
border: none;
}
.trade-tab:not(.v-tab--active) {
background-color: transparent;
border: none;
}
.maker-rebate {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
}
.maker-rebate-text {
font-size: 14px;
color: #ff9800;
font-weight: 500;
}
.order-book-content {
display: flex;
padding: 12px 16px;
gap: 16px;
}
.depth-chart {
width: 50%;
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: column;
gap: 4px;
}
.depth-chart-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.depth-bar-container {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
height: 24px;
}
.depth-bar {
border-radius: 2px 0 0 2px;
height: 80%;
min-width: 4px;
align-self: center;
}
.asks-bar {
background-color: #ffb3ba;
}
.bids-bar {
background-color: #b3ffb3;
}
.order-list {
flex: 1;
display: flex;
flex-direction: column;
max-height: 400px;
overflow-y: auto;
}
.order-list-header {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: none;
margin-bottom: 8px;
}
.order-list-header-price,
.order-list-header-shares,
.order-list-header-total {
font-size: 12px;
font-weight: 500;
color: #666666;
text-transform: uppercase;
}
.order-list-header-price {
width: 80px;
}
.order-list-header-shares {
flex: 1;
text-align: right;
}
.order-list-header-total {
width: 100px;
text-align: right;
}
.asks-label,
.bids-label {
font-size: 12px;
font-weight: 500;
margin: 8px 0 4px 0;
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
}
.asks-label {
color: #ffffff;
background-color: #ff0000;
}
.bids-label {
color: #ffffff;
background-color: #008000;
}
.order-item {
display: flex;
align-items: center;
padding: 0px 0;
border-bottom: none;
gap: 12px;
}
/* 深度图:固定宽度,进度条长度表示深度比例,不占满整行 */
.order-progress {
flex: 0 0 auto;
width: 120px;
min-width: 120px;
}
.asks-price {
color: #ff0000;
}
.bids-price {
color: #008000;
}
.order-price {
width: 80px;
font-size: 14px;
font-weight: 500;
}
.order-shares {
width: 100px;
text-align: right;
font-size: 14px;
color: #333333;
}
.order-total {
width: 100px;
text-align: right;
font-size: 14px;
color: #333333;
}
.bids-item {
background-color: transparent;
}
.order-book-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-top: none;
background-color: transparent;
}
.last-price,
.spread {
font-size: 14px;
color: #666666;
}
/* Responsive Adjustments */
@media (max-width: 600px) {
.order-book-content {
padding: 8px 12px;
gap: 12px;
}
.order-progress {
width: 80px;
min-width: 80px;
}
.order-list-header-price {
width: 60px;
}
.order-list-header-total {
width: 80px;
}
.order-price {
width: 60px;
font-size: 13px;
}
.order-shares {
font-size: 13px;
}
.order-total {
width: 80px;
font-size: 13px;
}
}
</style>