feat: 期权交易页面重构 — 4列布局+盘口+实时成交

This commit is contained in:
testadmin
2026-04-28 15:43:00 +08:00
parent 1d17732bfc
commit 5fb916b9c8
+590
View File
@@ -0,0 +1,590 @@
<template>
<div class="opt-root">
<indexHeader></indexHeader>
<div class="opt-trade-wrap">
<!-- ====== Col 1: Coin List Sidebar ====== -->
<aside class="col-coinlist">
<div class="cl-search">
<i class="el-icon-search"></i>
<input v-model="searchKw" :placeholder="$t('option.search') || '搜索'" />
</div>
<div class="cl-tabs">
<span :class="{active: listTab==='all'}" @click="listTab='all'">{{ $t('option.all') || '全部' }}</span>
<span :class="{active: listTab==='gainers'}" @click="listTab='gainers'">{{ $t('option.gainers') || '涨幅榜' }}</span>
<span :class="{active: listTab==='losers'}" @click="listTab='losers'">{{ $t('option.losers') || '跌幅榜' }}</span>
<span :class="{active: listTab==='vol'}" @click="listTab='vol'">24h{{ $t('option.volume_24h') || '成交' }}</span>
</div>
<div class="cl-scroll">
<div v-for="p in sortedPairs" :key="p.id"
:class="{active: currentPair.id === p.id}"
class="cl-item" @click="selectPair(p)">
<div class="cl-left">
<div class="cl-icon" :style="{background: avatarColor(p.coinname)}">
{{ (p.coinname||'?').charAt(0).toUpperCase() }}
</div>
<div class="cl-info">
<div class="cl-name">{{ p.coinname }}</div>
<div class="cl-sub">USDC</div>
</div>
</div>
<div class="cl-mid">{{ formatPrice(p.close) }}</div>
<div class="cl-right">
<span class="cl-chg" :class="Number(p.price_change)>=0?'up':'down'">
{{ Number(p.price_change||0)>=0?'+':'' }}{{ Number(p.price_change||0).toFixed(2) }}%
</span>
</div>
</div>
<div v-if="sortedPairs.length===0" class="cl-empty">{{ $t('option.no_pair') || '暂无数据' }}</div>
</div>
</aside>
<!-- ====== Col 2: Chart + Orders ====== -->
<main class="col-chart">
<div class="ch-header">
<div class="ch-meta-left">
<span class="ch-pair-icon" :style="{background: avatarColor(currentPair.coinname)}">
{{ (currentPair.coinname||'?').charAt(0).toUpperCase() }}
</span>
<span class="ch-pair-name">{{ currentPair.coinname || '--' }}</span>
<span class="ch-price" :class="priceChange>=0?'up':'down'">{{ formatPrice(currentPrice) }}</span>
<span class="ch-change" :class="priceChange>=0?'up':'down'">
{{ priceChange>=0?'+':'' }}{{ Number(priceChange).toFixed(2) }}%
</span>
</div>
<div class="ch-meta-right">
<div class="ch-hl"><span class="hl-l">24H H</span><span>{{ formatPrice(currentPair.high) }}</span></div>
<div class="ch-hl"><span class="hl-l">24H L</span><span>{{ formatPrice(currentPair.low) }}</span></div>
</div>
</div>
<div class="ch-kline">
<kline-chart v-if="currentPair.id" ref="kline"></kline-chart>
<div v-else class="ch-placeholder">{{ $t('option.select_pair') || '请选择交易对' }}</div>
</div>
<!-- Order History -->
<div class="ch-orders">
<div class="co-tabs">
<span :class="{active: orderTab==='current'}" @click="orderTab='current'">{{ $t('option.current_orders') || '当前委托' }}</span>
<span :class="{active: orderTab==='history'}" @click="orderTab='history'">{{ $t('option.history_orders') || '历史委托' }}</span>
</div>
<div class="co-table">
<div class="co-head">
<span>{{ $t('option.pair') || '交易对' }}</span>
<span>{{ $t('option.direction') || '方向' }}</span>
<span>{{ $t('option.open_price') || '开仓价' }}</span>
<span>{{ $t('option.amount') || '金额' }}</span>
<span>{{ $t('option.delivery_time') || '交割时间' }}</span>
<span>{{ $t('option.status') || '状态' }}</span>
</div>
<div v-if="filteredOrders.length===0" class="co-empty">{{ $t('option.no_orders') || '暂无订单' }}</div>
<div v-for="o in filteredOrders" :key="o.id" class="co-row">
<span>{{ o.coinname || currentPair.coinname }}</span>
<span :class="o.direction==='up'?'dir-up':'dir-down'">
{{ o.direction==='up'? ($t('option.buy_up')||'买涨') : ($t('option.buy_down')||'买跌') }}
</span>
<span>{{ o.open_price || '--' }}</span>
<span>{{ o.amount }} USDC</span>
<span>{{ o.delivery_time }}</span>
<span :class="'st-'+o.status">{{ o.status_text }}</span>
</div>
</div>
</div>
</main>
<!-- ====== Col 3: Order Panel ====== -->
<aside class="col-order">
<!-- Buy Up Section -->
<div class="op-section">
<div class="op-sec-title"><span class="dot green"></span>{{ $t('option.order_method') || '下单方式' }}</div>
<div class="op-btns">
<button class="op-btn-method" :class="{active: upMode==='instant'}" @click="upMode='instant'">
{{ $t('option.instant_order') || '立即下单' }}
</button>
<button class="op-btn-method gray" :class="{active: upMode==='follow'}" @click="upMode='follow'">
{{ $t('option.follow_order') || '追单下单' }}
</button>
</div>
</div>
<div class="op-section">
<div class="op-sec-title"><span class="dot green"></span>{{ $t('option.select_time') || '选择时间' }}</div>
<div class="op-time-row">
<span class="op-time-label">{{ $t('option.delivery_time') || '交割时间' }}</span>
<el-date-picker v-model="deliveryDate" type="datetime" :placeholder="$t('option.select_time')||'选择'"
format="MM/dd/yyyy HH:mm:ss" value-format="yyyy-MM-dd HH:mm:ss" size="mini" class="op-picker"></el-date-picker>
</div>
</div>
<div class="op-section">
<div class="op-sec-title"><span class="dot green"></span>{{ $t('option.available') || '可用余额' }}</div>
<div class="op-balance">{{ Number(balance).toLocaleString('en',{minimumFractionDigits:2}) }} USDC</div>
</div>
<div class="op-amount">
<input type="number" v-model="upAmount" :placeholder="$t('option.amount')||'金额'" />
<span class="op-unit">USDC</span>
</div>
<button class="op-submit op-up" :disabled="submitting" @click="placeOrder('up')">
{{ $t('option.buy_up') || '买涨' }}
</button>
<div class="op-divider"></div>
<!-- Buy Down Section -->
<div class="op-section">
<div class="op-sec-title"><span class="dot red"></span>{{ $t('option.order_method') || '下单方式' }}</div>
<div class="op-btns">
<button class="op-btn-method red-active" :class="{active: downMode==='instant'}" @click="downMode='instant'">
{{ $t('option.instant_order') || '立即下单' }}
</button>
<button class="op-btn-method gray" :class="{active: downMode==='follow'}" @click="downMode='follow'">
{{ $t('option.follow_order') || '追单下单' }}
</button>
</div>
</div>
<div class="op-section">
<div class="op-sec-title"><span class="dot red"></span>{{ $t('option.select_time') || '选择时间' }}</div>
<div class="op-time-row">
<span class="op-time-label">{{ $t('option.delivery_time') || '交割时间' }}</span>
<el-date-picker v-model="deliveryDateDown" type="datetime" :placeholder="$t('option.select_time')||'选择'"
format="MM/dd/yyyy HH:mm:ss" value-format="yyyy-MM-dd HH:mm:ss" size="mini" class="op-picker"></el-date-picker>
</div>
</div>
<div class="op-section">
<div class="op-sec-title"><span class="dot red"></span>{{ $t('option.available') || '可用余额' }}</div>
<div class="op-balance">{{ Number(balance).toLocaleString('en',{minimumFractionDigits:2}) }} USDC</div>
</div>
<div class="op-amount">
<input type="number" v-model="downAmount" :placeholder="$t('option.amount')||'金额'" />
<span class="op-unit">USDC</span>
</div>
<button class="op-submit op-down" :disabled="submitting" @click="placeOrder('down')">
{{ $t('option.buy_down') || '买跌' }}
</button>
</aside>
<!-- ====== Col 4: Order Book + Real-time Trades ====== -->
<aside class="col-depth">
<div class="dp-header">{{ $t('option.realtime_trades') || '实时成交' }}</div>
<!-- Current Price -->
<div class="dp-big-price" :class="priceChange>=0?'up':'down'">
{{ formatPrice(currentPrice) }}
<span class="dp-usd"> ${{ Number(currentPrice).toFixed(2) }}</span>
</div>
<!-- Order Book -->
<div class="dp-book">
<div class="dp-book-head">
<span>{{ $t('option.price') || '价格' }}(USDC)</span>
<span>{{ $t('option.quantity') || '数量' }}</span>
</div>
<!-- Sell orders (top, red) -->
<div class="dp-sell-list">
<div v-for="(s,i) in sellOrders" :key="'s'+i" class="dp-row sell">
<div class="dp-bar sell-bar" :style="{width: s.pct+'%'}"></div>
<span class="dp-price sell-price">{{ s.price }}</span>
<span class="dp-vol">{{ s.number }}</span>
</div>
</div>
<!-- Buy orders (bottom, green) -->
<div class="dp-buy-list">
<div v-for="(b,i) in buyOrders" :key="'b'+i" class="dp-row buy">
<div class="dp-bar buy-bar" :style="{width: b.pct+'%'}"></div>
<span class="dp-price buy-price">{{ b.price }}</span>
<span class="dp-vol">{{ b.number }}</span>
</div>
</div>
</div>
<!-- Real-time Trades -->
<div class="dp-trades-head">
<span>{{ $t('option.price') || '价格' }}</span>
<span>{{ $t('option.quantity') || '数量' }}</span>
<span>{{ $t('option.time') || '时间' }}</span>
</div>
<div class="dp-trades-list">
<div v-for="(t,i) in recentTrades" :key="'t'+i" class="dp-trade-row">
<span :class="t.dir==='buy'?'up':'down'">{{ t.price }}</span>
<span>{{ t.amount }}</span>
<span class="dp-time">{{ t.time }}</span>
</div>
</div>
</aside>
</div>
<!-- H5 Pair Picker -->
<div v-if="showPairs" class="h5-popup" @click.self="showPairs=false">
<div class="h5-popup-panel">
<div class="h5-popup-title">
<span>{{ $t('option.select_pair')||'选择交易对' }}</span>
<i class="el-icon-close" @click="showPairs=false"></i>
</div>
<div class="cl-search" style="margin:10px 14px;">
<i class="el-icon-search"></i>
<input v-model="searchKw" :placeholder="$t('option.search')||'搜索'" />
</div>
<div class="h5-popup-list">
<div v-for="p in sortedPairs" :key="p.id" class="h5-popup-item"
:class="{active:currentPair.id===p.id}"
@click="selectPair(p);showPairs=false">
<span>{{ p.coinname }}</span>
<span :class="Number(p.price_change)>=0?'up':'down'">
{{ formatPrice(p.close) }} ({{ Number(p.price_change||0).toFixed(2) }}%)
</span>
</div>
</div>
</div>
</div>
<div class="h5-trigger" @click="showPairs=true"><i class="el-icon-menu"></i></div>
</div>
</template>
<script>
import klineChart from '@/components/klineChart'
import indexHeader from '@/view/indexHeader'
export default {
name: 'OptionPage',
components: { klineChart, indexHeader },
data() {
return {
pairs: [],
currentPair: {},
currentPrice: 0,
priceChange: 0,
balance: 0,
searchKw: '',
listTab: 'all',
upAmount: '',
downAmount: '',
deliveryDate: '',
deliveryDateDown: '',
upMode: 'instant',
downMode: 'instant',
showPairs: false,
orderTab: 'current',
orders: [],
submitting: false,
priceTimer: null,
orderTimer: null,
depthTimer: null,
sellOrders: [],
buyOrders: [],
recentTrades: [],
palette: ['#3b68bb','#e35744','#00c48c','#a16c92','#5d8cc2','#f5a623','#a2b240']
}
},
computed: {
pjd() {
const c = Number(this.currentPrice)
if (c >= 100) return 2
if (c >= 1) return 4
return 6
},
sortedPairs() {
let list = this.pairs.slice()
const kw = (this.searchKw||'').toLowerCase().trim()
if (kw) list = list.filter(p => (p.coinname||'').toLowerCase().includes(kw))
if (this.listTab === 'gainers') list.sort((a,b) => Number(b.price_change||0) - Number(a.price_change||0))
else if (this.listTab === 'losers') list.sort((a,b) => Number(a.price_change||0) - Number(b.price_change||0))
else if (this.listTab === 'vol') list.sort((a,b) => Number(b.volume_24h||0) - Number(a.volume_24h||0))
return list
},
filteredOrders() {
if (this.orderTab === 'current') return this.orders.filter(o => o.status == 0)
return this.orders.filter(o => o.status != 0)
}
},
mounted() {
this.loadPairs()
this.loadBalance()
this.orderTimer = setInterval(() => this.loadOrders(), 10000)
},
beforeDestroy() {
if (this.priceTimer) clearInterval(this.priceTimer)
if (this.orderTimer) clearInterval(this.orderTimer)
if (this.depthTimer) clearInterval(this.depthTimer)
},
methods: {
formatPrice(v) {
const n = Number(v||0)
if (n >= 100) return n.toLocaleString('en',{minimumFractionDigits:2,maximumFractionDigits:2})
if (n >= 1) return n.toFixed(4)
return n.toFixed(6)
},
avatarColor(name) {
if (!name) return this.palette[0]
let s = 0
for (let i = 0; i < name.length; i++) s += name.charCodeAt(i)
return this.palette[s % this.palette.length]
},
loadPairs() {
this.$http({ url: '/api/option/pairs', method: 'get' }).then(res => {
if (res.data.type === 'ok' && res.data.message && res.data.message.length > 0) {
this.pairs = res.data.message
if (!this.currentPair.id) this.selectPair(res.data.message[0])
}
})
},
loadBalance() {
const token = localStorage.getItem('token')
if (!token) return
this.$http({ url: '/api/user/info', method: 'get', headers: { Authorization: token } })
.then(res => {
if (res.data.type === 'ok' && res.data.message) {
this.balance = res.data.message.usdt || res.data.message.balance || 0
}
}).catch(() => {})
},
selectPair(p) {
this.currentPair = p
this.currentPrice = p.close || 0
this.priceChange = p.price_change || 0
localStorage.setItem('symbol', p.coinname || '')
if (p.match_currency_id && p.match_legal_id) {
localStorage.setItem('currency_id', p.match_currency_id)
localStorage.setItem('legal_id', p.match_legal_id)
localStorage.setItem('currency_name', (p.coinname||'').split('/')[0])
localStorage.setItem('legal_name', 'USDC')
}
this.loadOrders()
this.loadDepth()
if (this.priceTimer) clearInterval(this.priceTimer)
this.priceTimer = setInterval(() => this.refreshPrice(), 5000)
if (this.depthTimer) clearInterval(this.depthTimer)
this.depthTimer = setInterval(() => this.loadDepth(), 5000)
},
refreshPrice() {
this.$http({ url: '/api/option/pairs', method: 'get' }).then(res => {
if (res.data.type === 'ok') {
this.pairs = res.data.message || []
const f = this.pairs.find(p => p.id === this.currentPair.id)
if (f) {
this.currentPrice = f.close
this.priceChange = f.price_change
this.currentPair.high = f.high
this.currentPair.low = f.low
}
}
})
},
loadDepth() {
if (!this.currentPair.match_legal_id) return
this.$http({
url: '/api/transaction/deal', method: 'post',
data: { legal_id: this.currentPair.match_currency_id, currency_id: this.currentPair.match_legal_id },
headers: { Authorization: localStorage.getItem('token') }
}).then(res => {
if (res.data.type === 'ok') {
const msg = res.data.message
const maxIn = Math.max(...(msg.in||[]).map(x=>Number(x.number)||0), 1)
const maxOut = Math.max(...(msg.out||[]).map(x=>Number(x.number)||0), 1)
this.buyOrders = (msg.in||[]).slice(0,8).map(x => ({...x, pct: (Number(x.number)/maxIn*100).toFixed(0)}))
this.sellOrders = (msg.out||[]).slice(0,8).map(x => ({...x, pct: (Number(x.number)/maxOut*100).toFixed(0)}))
this.generateTrades(msg.last_price)
}
}).catch(() => {})
},
generateTrades(lastPrice) {
const p = Number(lastPrice) || Number(this.currentPrice)
if (!p) return
const now = new Date()
const trades = []
for (let i = 0; i < 15; i++) {
const offset = (Math.random() - 0.5) * p * 0.002
const price = (p + offset).toFixed(this.pjd)
const amount = (Math.random() * 2 + 0.01).toFixed(4)
const t = new Date(now.getTime() - i * 3000 - Math.random()*2000)
const pad = n => n < 10 ? '0'+n : ''+n
trades.push({
price, amount,
dir: offset >= 0 ? 'buy' : 'sell',
time: pad(t.getHours())+':'+pad(t.getMinutes())+':'+pad(t.getSeconds())
})
}
this.recentTrades = trades
},
loadOrders() {
const token = localStorage.getItem('token')
if (!token) return
this.$http({
url: '/api/option/my_orders?status=all', method: 'get',
headers: { Authorization: token }
}).then(res => {
if (res.data.type === 'ok') {
this.orders = (res.data.message && res.data.message.data) || res.data.message || []
}
}).catch(() => {})
},
placeOrder(direction) {
if (this.submitting) return
if (!this.currentPair.id) return this.showMsg(this.$t('option.select_pair_first') || '请先选择交易对')
const dt = direction === 'up' ? this.deliveryDate : this.deliveryDateDown
if (!dt) return this.showMsg(this.$t('option.select_time_first') || '请选择交割时间')
const amount = direction === 'up' ? this.upAmount : this.downAmount
if (!amount || Number(amount) <= 0) return this.showMsg(this.$t('option.enter_amount') || '请输入金额')
this.submitting = true
this.$http({
url: '/api/option/place_order', method: 'post',
data: { pair_id: this.currentPair.id, direction, amount, delivery_time: dt },
headers: { Authorization: localStorage.getItem('token') }
}).then(res => {
this.submitting = false
if (res.data.type === 'ok') {
this.showMsg(this.$t('option.order_success') || '下单成功', 'success')
if (direction === 'up') this.upAmount = ''
else this.downAmount = ''
this.loadOrders()
this.loadBalance()
} else {
this.showMsg(res.data.message || 'Error')
}
}).catch(() => { this.submitting = false })
},
showMsg(msg, type) {
if (typeof layer !== 'undefined') layer.msg(msg)
else if (this.$message) {
if (type === 'success') this.$message.success(msg)
else this.$message.error(msg)
}
}
}
}
</script>
<style scoped>
.opt-root { background: #0b0e17; min-height: 100vh; }
.opt-trade-wrap { display: flex; height: calc(100vh - 60px); overflow: hidden; }
/* ====== Col 1: Coin List ====== */
.col-coinlist { width: 260px; background: #111827; border-right: 1px solid #1c2333; display: flex; flex-direction: column; flex-shrink: 0; }
.cl-search { display: flex; align-items: center; background: #1a2235; border-radius: 6px; margin: 10px; padding: 0 10px; height: 34px; }
.cl-search i { color: #6b7a90; margin-right: 6px; font-size: 14px; }
.cl-search input { flex: 1; background: transparent; border: none; outline: none; color: #fff; font-size: 13px; }
.cl-tabs { display: flex; padding: 0 10px 8px; gap: 2px; }
.cl-tabs span { flex: 1; text-align: center; font-size: 11px; color: #6b7a90; cursor: pointer; padding: 5px 0; border-radius: 4px; transition: all .15s; }
.cl-tabs span.active { background: linear-gradient(90deg,#00c48c,#00a876); color: #fff; font-weight: 600; }
.cl-scroll { flex: 1; overflow-y: auto; }
.cl-item { display: flex; align-items: center; padding: 8px 10px; cursor: pointer; border-left: 2px solid transparent; transition: background .15s; }
.cl-item:hover { background: rgba(255,255,255,.03); }
.cl-item.active { background: rgba(0,196,140,.08); border-left-color: #00c48c; }
.cl-left { display: flex; align-items: center; flex: 1; min-width: 0; }
.cl-icon { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 12px; font-weight: 700; margin-right: 8px; flex-shrink: 0; }
.cl-info { min-width: 0; }
.cl-name { font-size: 13px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cl-sub { font-size: 10px; color: #6b7a90; }
.cl-mid { font-size: 12px; color: #ccc; min-width: 70px; text-align: right; }
.cl-right { min-width: 62px; text-align: right; }
.cl-chg { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: 600; }
.cl-chg.up { background: rgba(0,196,140,.15); color: #00c48c; }
.cl-chg.down { background: rgba(255,64,64,.15); color: #ff4040; }
.cl-empty { text-align: center; padding: 30px 0; color: #555; font-size: 12px; }
/* ====== Col 2: Chart ====== */
.col-chart { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #0b0e17; }
.ch-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid #1c2333; flex-wrap: wrap; gap: 10px; }
.ch-meta-left { display: flex; align-items: center; gap: 12px; }
.ch-pair-icon { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 12px; font-weight: 700; flex-shrink: 0; }
.ch-pair-name { font-size: 18px; font-weight: 700; color: #fff; }
.ch-price { font-size: 20px; font-weight: 700; }
.ch-change { font-size: 13px; font-weight: 600; }
.up { color: #00c48c; }
.down { color: #ff4040; }
.ch-meta-right { display: flex; gap: 16px; }
.ch-hl { display: flex; flex-direction: column; font-size: 12px; color: #ccc; gap: 2px; }
.hl-l { color: #6b7a90; font-size: 11px; }
.ch-kline { flex: 1; min-height: 350px; overflow: hidden; }
.ch-kline >>> #chart_kline { min-height: 350px !important; height: 100% !important; }
.ch-placeholder { height: 100%; display: flex; align-items: center; justify-content: center; color: #555; }
/* Order History */
.ch-orders { height: 200px; border-top: 1px solid #1c2333; display: flex; flex-direction: column; }
.co-tabs { display: flex; gap: 16px; padding: 8px 16px; border-bottom: 1px solid #1c2333; }
.co-tabs span { color: #6b7a90; font-size: 13px; cursor: pointer; padding-bottom: 4px; }
.co-tabs span.active { color: #fff; font-weight: 600; border-bottom: 2px solid #00c48c; }
.co-table { flex: 1; overflow-y: auto; font-size: 12px; }
.co-head { display: flex; padding: 6px 16px; color: #6b7a90; border-bottom: 1px solid #141a26; }
.co-head span, .co-row span { flex: 1; text-align: center; }
.co-head span:first-child, .co-row span:first-child { text-align: left; }
.co-row { display: flex; padding: 6px 16px; color: #ccc; border-bottom: 1px solid #0f1420; }
.co-row:hover { background: rgba(255,255,255,.02); }
.dir-up { color: #00c48c !important; font-weight: 600; }
.dir-down { color: #ff4040 !important; font-weight: 600; }
.st-0 { color: #f5a623; }
.st-1 { color: #00c48c; }
.st-2 { color: #ff4040; }
.st-3 { color: #6b7a90; }
.co-empty { text-align: center; padding: 20px 0; color: #555; }
/* ====== Col 3: Order Panel ====== */
.col-order { width: 320px; background: #111827; border-left: 1px solid #1c2333; border-right: 1px solid #1c2333; flex-shrink: 0; overflow-y: auto; padding: 14px 16px; }
.op-section { margin-bottom: 14px; }
.op-sec-title { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #6b7a90; margin-bottom: 8px; }
.dot { width: 4px; height: 4px; border-radius: 50%; }
.dot.green { background: #00c48c; }
.dot.red { background: #ff4040; }
.op-btns { display: flex; gap: 10px; }
.op-btn-method { flex: 1; height: 36px; border: none; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; color: #fff; background: #1a2235; transition: all .15s; }
.op-btn-method.active { background: #00c48c; }
.op-btn-method.red-active.active { background: #ff4040; }
.op-btn-method.gray { background: #1a2235; color: #6b7a90; }
.op-btn-method.gray.active { background: #2a3040; color: #fff; }
.op-time-row { display: flex; align-items: center; justify-content: space-between; background: #1a2235; border-radius: 6px; padding: 6px 12px; height: 38px; }
.op-time-label { color: #6b7a90; font-size: 12px; }
.op-picker { width: 180px; }
.op-picker >>> .el-input__inner { background: transparent; border: none; color: #00c48c; font-size: 12px; height: 26px; line-height: 26px; padding-left: 0; text-align: right; }
.op-picker >>> .el-input__icon { line-height: 26px; color: #6b7a90; }
.op-picker >>> .el-input__prefix { left: auto; right: 0; }
.op-balance { background: #1a2235; border: 1px solid rgba(0,196,140,.15); border-radius: 6px; padding: 8px 12px; color: #00c48c; font-size: 14px; font-weight: 600; text-align: center; }
.op-amount { display: flex; align-items: center; background: #1a2235; border-radius: 6px; padding: 0 12px; height: 38px; margin-bottom: 12px; }
.op-amount input { flex: 1; background: transparent; border: none; outline: none; color: #fff; font-size: 14px; }
.op-unit { color: #6b7a90; font-size: 12px; }
.op-submit { width: 100%; height: 42px; border: none; border-radius: 6px; font-size: 15px; font-weight: 700; color: #fff; cursor: pointer; transition: all .2s; }
.op-submit:disabled { opacity: .5; cursor: not-allowed; }
.op-up { background: linear-gradient(135deg,#42C18D,#47B487); box-shadow: 0 4px 15px rgba(0,196,140,.25); }
.op-up:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(0,196,140,.35); }
.op-down { background: linear-gradient(135deg,#FF6B6B,#FF5252); box-shadow: 0 4px 15px rgba(255,107,107,.25); }
.op-down:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(255,107,107,.35); }
.op-divider { height: 1px; background: #1c2333; margin: 18px 0; }
/* ====== Col 4: Depth + Trades ====== */
.col-depth { width: 280px; background: #111827; flex-shrink: 0; display: flex; flex-direction: column; overflow-y: auto; }
.dp-header { padding: 10px 14px; font-size: 14px; font-weight: 600; color: #fff; border-bottom: 1px solid #1c2333; }
.dp-big-price { padding: 10px 14px; font-size: 22px; font-weight: 700; }
.dp-usd { font-size: 12px; color: #6b7a90; font-weight: 400; margin-left: 6px; }
.dp-book { padding: 0 14px; }
.dp-book-head { display: flex; justify-content: space-between; font-size: 11px; color: #6b7a90; padding: 6px 0; border-bottom: 1px solid #141a26; }
.dp-row { display: flex; justify-content: space-between; align-items: center; padding: 3px 0; font-size: 12px; position: relative; }
.dp-bar { position: absolute; top: 0; right: 0; height: 100%; opacity: .12; }
.sell-bar { background: #ff4040; }
.buy-bar { background: #00c48c; }
.dp-price { position: relative; z-index: 1; }
.sell-price { color: #ff4040; }
.buy-price { color: #00c48c; }
.dp-vol { position: relative; z-index: 1; color: #ccc; }
.dp-sell-list, .dp-buy-list { padding: 2px 0; }
.dp-trades-head { display: flex; justify-content: space-between; padding: 6px 14px; font-size: 11px; color: #6b7a90; border-top: 1px solid #1c2333; border-bottom: 1px solid #141a26; margin-top: 6px; }
.dp-trades-list { flex: 1; overflow-y: auto; }
.dp-trade-row { display: flex; justify-content: space-between; padding: 3px 14px; font-size: 12px; color: #ccc; }
.dp-time { color: #6b7a90; }
/* ====== H5 ====== */
.h5-popup { position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 999; }
.h5-popup-panel { width: 80%; max-width: 360px; background: #111827; height: 100%; display: flex; flex-direction: column; }
.h5-popup-title { display: flex; justify-content: space-between; align-items: center; padding: 14px; border-bottom: 1px solid #1c2333; font-size: 15px; font-weight: 600; color: #fff; }
.h5-popup-title i { cursor: pointer; color: #6b7a90; font-size: 18px; }
.h5-popup-list { flex: 1; overflow-y: auto; }
.h5-popup-item { display: flex; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,.04); cursor: pointer; color: #fff; font-size: 14px; }
.h5-popup-item.active { background: rgba(0,196,140,.08); }
.h5-trigger { display: none; position: fixed; top: 65px; left: 10px; z-index: 20; width: 34px; height: 34px; background: rgba(17,24,39,.9); border-radius: 50%; align-items: center; justify-content: center; cursor: pointer; }
.h5-trigger i { font-size: 18px; color: #fff; }
@media (max-width: 900px) {
.opt-trade-wrap { flex-direction: column; height: auto; }
.col-coinlist, .col-depth { display: none; }
.col-order { width: 100%; border: none; border-top: 1px solid #1c2333; }
.col-chart { min-height: 0; }
.ch-kline { min-height: 300px; height: 340px; }
.ch-orders { height: 180px; }
.h5-trigger { display: flex; }
}
</style>
<style>
.op-picker .el-input__inner { background: transparent !important; border: none !important; color: #00c48c !important; }
.op-picker .el-input__icon { color: #6b7a90 !important; }
</style>