lucky-loster's picture
Upload folder using huggingface_hub
590a501 verified
Raw
History Blame Contribute Delete
9.68 kB
<template>
<div>
<!-- Top bar -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<div class="cat-tabs">
<button v-for="cat in store.categoryList" :key="cat.key" :class="['cat-tab', { active: selectedCategory === cat.key }]" @click="selectedCategory = cat.key; filterQuotes()">{{ cat.label }}</button>
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<el-switch v-model="isRealtime" active-text="实时" inactive-text="模拟" @change="toggleMode" />
<div :class="['tag-pill', store.marketMode === 'realtime' ? 'bullish' : 'neutral']">{{ store.marketMode === 'realtime' ? '实时数据' : '模拟数据' }}</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 260px 1fr; gap: 16px;">
<!-- Contract list -->
<div class="mk-card" style="padding: 0; overflow: hidden;">
<div style="padding: 14px 16px; border-bottom: 1px solid var(--border-light);"><span style="font-weight: 700; font-size: 14px; color: var(--text-dark);">合约列表 ({{ displayedQuotes.length }})</span></div>
<div style="padding: 8px 12px;">
<el-input v-model="searchText" placeholder="搜索..." size="small" clearable><template #prefix><el-icon><Search /></el-icon></template></el-input>
</div>
<div style="max-height: 540px; overflow-y: auto; padding: 0 8px 8px;">
<div v-for="q in displayedQuotes" :key="q.symbol" @click="selectSymbol(q.symbol)"
:style="{ padding: '10px 12px', cursor: 'pointer', borderRadius: '8px', marginBottom: '3px', background: selectedSymbol === q.symbol ? 'var(--green-light)' : 'transparent', border: selectedSymbol === q.symbol ? '1px solid rgba(76,175,80,0.3)' : '1px solid transparent', transition: 'all 0.2s' }">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<span style="font-weight: 600; font-size: 13px;">{{ q.name || q.symbol }}</span>
<span style="font-size: 10px; color: var(--text-muted); margin-left: 4px;">{{ q.exchange }}</span>
</div>
<span :class="q.change_pct >= 0 ? 'price-up' : 'price-down'" style="font-weight: 700; font-size: 13px;">{{ q.close?.toFixed(2) }}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 3px;">
<span style="font-size: 10px; color: var(--text-muted);">Vol {{ q.volume?.toLocaleString() }}</span>
<span :class="q.change_pct >= 0 ? 'price-up' : 'price-down'" style="font-size: 11px; font-weight: 600;">{{ q.change_pct ? (q.change_pct * 100).toFixed(2) + '%' : '-' }}</span>
</div>
</div>
</div>
</div>
<!-- Chart + detail -->
<div style="display: flex; flex-direction: column; gap: 16px;">
<div class="mk-card" style="padding: 0; overflow: hidden;">
<div style="padding: 14px 20px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600;">{{ currentQuoteName }}</span>
<div class="interval-group">
<button v-for="tf in timeframes" :key="tf.v" :class="['interval-btn', { active: selectedInterval === tf.v }]" @click="selectedInterval = tf.v; loadKlines()">{{ tf.l }}</button>
</div>
</div>
<div style="padding: 8px 12px;">
<v-chart :option="chartOption" autoresize style="height: 420px;" />
</div>
</div>
<div class="mk-card" style="padding: 0; overflow: hidden;" v-if="currentQuote">
<div style="padding: 12px 20px; border-bottom: 1px solid var(--border-light);"><span style="font-weight: 700; font-size: 14px; color: var(--text-dark);">行情详情</span></div>
<div style="padding: 16px 20px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;">
<div v-for="item in detailItems" :key="item.label">
<div style="font-size: 11px; color: var(--text-muted); margin-bottom: 4px;">{{ item.label }}</div>
<div :style="{ fontSize: '14px', fontWeight: 600, color: item.color || 'var(--text-primary)' }">{{ item.value }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CandlestickChart, BarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, DataZoomComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import { useTradingStore } from '../stores/trading'
import { marketApi } from '../api'
use([CandlestickChart, BarChart, GridComponent, TooltipComponent, DataZoomComponent, CanvasRenderer])
const store = useTradingStore()
const selectedSymbol = ref('')
const selectedCategory = ref('')
const selectedInterval = ref('15m')
const searchText = ref('')
const klines = ref([])
const isRealtime = ref(false)
const timeframes = [
{ l: '1分', v: '1m' }, { l: '5分', v: '5m' }, { l: '15分', v: '15m' }, { l: '30分', v: '30m' },
{ l: '1时', v: '1h' }, { l: '4时', v: '4h' }, { l: '日线', v: '1d' }, { l: '周线', v: '1w' },
]
const displayedQuotes = computed(() => {
let qs = [...store.quotes]
if (selectedCategory.value) qs = qs.filter(q => q.category === selectedCategory.value)
if (searchText.value) { const s = searchText.value.toLowerCase(); qs = qs.filter(q => (q.name || '').toLowerCase().includes(s) || q.symbol.toLowerCase().includes(s)) }
return qs.sort((a, b) => (a.name || a.symbol).localeCompare(b.name || b.symbol, 'zh'))
})
const currentQuote = computed(() => store.quotes.find(q => q.symbol === selectedSymbol.value))
const currentQuoteName = computed(() => { const q = currentQuote.value; return q ? `${q.name} (${q.symbol})` : selectedSymbol.value })
const detailItems = computed(() => {
const q = currentQuote.value; if (!q) return []
const pctColor = q.change_pct >= 0 ? 'var(--green)' : 'var(--red)'
return [
{ label: '最新价', value: q.close?.toFixed(2), color: pctColor },
{ label: '涨跌幅', value: q.change_pct ? (q.change_pct * 100).toFixed(2) + '%' : '-', color: pctColor },
{ label: '开盘价', value: q.open?.toFixed(2) }, { label: '昨收', value: q.pre_close?.toFixed(2) || '-' },
{ label: '最高价', value: q.high?.toFixed(2) }, { label: '最低价', value: q.low?.toFixed(2) },
{ label: '买价', value: q.bid?.toFixed(2) }, { label: '卖价', value: q.ask?.toFixed(2) },
{ label: '成交量', value: q.volume?.toLocaleString() }, { label: '持仓量', value: q.open_interest?.toLocaleString() || '-' },
{ label: '交易所', value: q.exchange }, { label: '分类', value: q.category },
]
})
async function loadKlines() {
if (!selectedSymbol.value) return
try { const { data } = await marketApi.getKlines(selectedSymbol.value, 200, selectedInterval.value); klines.value = data } catch (e) { console.error(e) }
}
function selectSymbol(s) { selectedSymbol.value = s; loadKlines() }
function filterQuotes() { store.fetchQuotes(selectedCategory.value ? { category: selectedCategory.value } : {}) }
async function toggleMode(v) { await store.setMarketMode(v ? 'realtime' : 'simulated') }
const chartOption = computed(() => {
const dates = klines.value.map(k => {
const t = k.timestamp || ''
if (['1d','1w'].includes(selectedInterval.value)) return t.substring(5, 10)
if (['4h','1h'].includes(selectedInterval.value)) return t.substring(5, 16)
return t.substring(11, 16)
})
const ohlc = klines.value.map(k => [k.open, k.close, k.low, k.high])
const volumes = klines.value.map(k => k.volume)
return {
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
grid: [{ left: '8%', right: '3%', top: '4%', height: '58%' }, { left: '8%', right: '3%', top: '68%', height: '18%' }],
xAxis: [
{ type: 'category', data: dates, gridIndex: 0, axisLine: { lineStyle: { color: '#e9ecef' } }, axisLabel: { color: '#8392ab', fontSize: 10 } },
{ type: 'category', data: dates, gridIndex: 1, axisLine: { lineStyle: { color: '#e9ecef' } }, axisLabel: { color: '#8392ab', fontSize: 10 } },
],
yAxis: [
{ scale: true, gridIndex: 0, splitLine: { lineStyle: { color: '#f0f2f5' } }, axisLabel: { color: '#8392ab', fontSize: 10 } },
{ scale: true, gridIndex: 1, splitLine: { lineStyle: { color: '#f0f2f5' } }, axisLabel: { color: '#8392ab', fontSize: 10 } },
],
dataZoom: [
{ type: 'inside', xAxisIndex: [0, 1], start: 30, end: 100 },
{ type: 'slider', xAxisIndex: [0, 1], start: 30, end: 100, height: 18, bottom: 2, borderColor: '#e9ecef', fillerColor: 'rgba(76,175,80,0.08)', handleStyle: { color: '#4caf50' }, textStyle: { color: '#8392ab' } },
],
series: [
{ type: 'candlestick', data: ohlc, xAxisIndex: 0, yAxisIndex: 0, itemStyle: { color: '#4caf50', color0: '#f44336', borderColor: '#4caf50', borderColor0: '#f44336' } },
{ type: 'bar', data: volumes, xAxisIndex: 1, yAxisIndex: 1, itemStyle: { color: 'rgba(76,175,80,0.25)' } },
],
}
})
watch(selectedSymbol, loadKlines)
onMounted(async () => {
await store.fetchCategories(); await store.fetchQuotes(); await store.fetchMarketMode()
isRealtime.value = store.marketMode === 'realtime'
if (store.quotes.length > 0 && !selectedSymbol.value) { selectedSymbol.value = store.quotes[0].symbol; loadKlines() }
})
</script>