487 lines
10 KiB
Vue
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>
|