| <template> |
| <div style="min-height: 100vh; background: var(--bg-body);" @click="openMenu = ''"> |
| |
| <nav class="top-navbar"> |
| <div class="nav-inner"> |
| |
| <a class="nav-brand" @click.prevent="go('/')"> |
| <div class="brand-icon">P</div> |
| <span class="brand-text">PTS V1</span> |
| </a> |
| |
| |
| <div class="nav-links"> |
| |
| <div class="nav-dropdown"> |
| <a class="nav-link" @click.stop="toggleMenu('market')"> |
| <el-icon><TrendCharts /></el-icon> |
| 行情 |
| <span class="nav-arrow" :class="{ open: openMenu === 'market' }">▾</span> |
| </a> |
| <div class="dropdown-panel" v-show="openMenu === 'market'" @click.stop> |
| <a class="dd-item" @click="go('/')"><el-icon><Monitor /></el-icon> 仪表盘</a> |
| <a class="dd-item" @click="go('/market')"><el-icon><TrendCharts /></el-icon> 行情中心</a> |
| </div> |
| </div> |
| |
| |
| <div class="nav-dropdown"> |
| <a class="nav-link" @click.stop="toggleMenu('trade')"> |
| <el-icon><Sell /></el-icon> |
| 交易 |
| <span class="nav-arrow" :class="{ open: openMenu === 'trade' }">▾</span> |
| </a> |
| <div class="dropdown-panel" v-show="openMenu === 'trade'" @click.stop> |
| <a class="dd-item" @click="go('/trading')"><el-icon><Sell /></el-icon> 交易下单</a> |
| <a class="dd-item" @click="go('/positions')"><el-icon><Wallet /></el-icon> 持仓管理</a> |
| </div> |
| </div> |
| |
| |
| <div class="nav-dropdown"> |
| <a class="nav-link" @click.stop="toggleMenu('strategy')"> |
| <el-icon><DataAnalysis /></el-icon> |
| 策略 |
| <span class="nav-arrow" :class="{ open: openMenu === 'strategy' }">▾</span> |
| </a> |
| <div class="dropdown-panel" v-show="openMenu === 'strategy'" @click.stop> |
| <a class="dd-item" @click="go('/strategies')"><el-icon><DataAnalysis /></el-icon> 量化策略</a> |
| <a class="dd-item" @click="go('/backtest')"><el-icon><DataLine /></el-icon> 策略回测</a> |
| <a class="dd-item" @click="go('/opportunities')"><el-icon><Aim /></el-icon> 寻找机会</a> |
| </div> |
| </div> |
| |
| |
| <a class="nav-link" @click.prevent="go('/risk')"> |
| <el-icon><Warning /></el-icon> |
| 风控中心 |
| </a> |
| |
| |
| <div class="nav-dropdown"> |
| <a class="nav-link" @click.stop="toggleMenu('platform')"> |
| <el-icon><Setting /></el-icon> |
| 平台 |
| <span class="nav-arrow" :class="{ open: openMenu === 'platform' }">▾</span> |
| </a> |
| <div class="dropdown-panel" v-show="openMenu === 'platform'" @click.stop> |
| <a class="dd-item" @click="go('/services')"><el-icon><Setting /></el-icon> 服务控制台</a> |
| <a class="dd-item" @click="go('/research')"><el-icon><Cpu /></el-icon> 因子研究</a> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="nav-right"> |
| <span class="badge-pill green" style="margin-right: 12px;">{{ store.marketMode === 'realtime' ? '实时' : '模拟' }}</span> |
| <div class="nav-account"> |
| <span class="account-label">总资产</span> |
| <span class="account-value">¥{{ fmt(store.account.total_balance) }}</span> |
| </div> |
| <div class="nav-divider"></div> |
| <div class="nav-account"> |
| <span class="account-label">盈亏</span> |
| <span class="account-value" :style="{ color: store.account.unrealized_pnl >= 0 ? 'var(--green)' : 'var(--red)' }"> |
| {{ store.account.unrealized_pnl >= 0 ? '+' : '' }}¥{{ fmt(store.account.unrealized_pnl) }} |
| </span> |
| </div> |
| </div> |
| </div> |
| </nav> |
| |
| |
| <main style="padding: 24px 32px; max-width: 1600px; margin: 0 auto;"> |
| <router-view /> |
| </main> |
| </div> |
| </template> |
| |
| <script setup> |
| import { ref, onMounted, onUnmounted } from 'vue' |
| import { useRouter } from 'vue-router' |
| import { useTradingStore } from './stores/trading' |
| |
| const router = useRouter() |
| const store = useTradingStore() |
| const openMenu = ref('') |
| |
| function toggleMenu(name) { |
| openMenu.value = openMenu.value === name ? '' : name |
| } |
| |
| function go(path) { |
| openMenu.value = '' |
| router.push(path) |
| } |
| |
| function fmt(n) { return (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } |
| |
| let iv |
| onMounted(() => { |
| store.fetchCategories(); store.fetchContracts(); store.fetchQuotes() |
| store.fetchAccount(); store.fetchMarketMode(); store.connectWebSocket() |
| iv = setInterval(() => { store.fetchAccount(); store.fetchQuotes() }, 3000) |
| }) |
| onUnmounted(() => { store.disconnectWebSocket(); clearInterval(iv) }) |
| </script> |
| |
| <style scoped> |
| .top-navbar { |
| background: rgba(255,255,255,0.95); |
| backdrop-filter: blur(12px); |
| -webkit-backdrop-filter: blur(12px); |
| box-shadow: 0 2px 12px rgba(0,0,0,0.06); |
| position: sticky; top: 0; z-index: 100; |
| border-bottom: 1px solid rgba(0,0,0,0.05); |
| } |
| .nav-inner { |
| max-width: 1600px; margin: 0 auto; |
| display: flex; align-items: center; justify-content: space-between; |
| padding: 0 32px; height: 60px; |
| } |
| .nav-brand { |
| display: flex; align-items: center; gap: 10px; text-decoration: none; cursor: pointer; |
| } |
| .brand-icon { |
| width: 34px; height: 34px; border-radius: 8px; |
| background: var(--green-gradient); color: white; |
| display: flex; align-items: center; justify-content: center; |
| font-weight: 700; font-size: 15px; |
| box-shadow: 0 3px 5px -1px rgba(76,175,80,0.4); |
| } |
| .brand-text { font-size: 15px; font-weight: 700; color: var(--text-dark); letter-spacing: 0.5px; } |
| .nav-links { display: flex; align-items: center; gap: 4px; } |
| .nav-dropdown { position: relative; } |
| .nav-link { |
| display: flex; align-items: center; gap: 6px; |
| padding: 8px 16px; border-radius: 8px; |
| font-size: 14px; font-weight: 500; color: var(--text-body); |
| cursor: pointer; text-decoration: none; transition: all 0.15s; |
| user-select: none; |
| } |
| .nav-link:hover { color: var(--text-dark); background: var(--bg-light); } |
| .nav-arrow { |
| font-size: 10px; opacity: 0.5; margin-left: 2px; |
| transition: transform 0.2s; display: inline-block; |
| } |
| .nav-arrow.open { transform: rotate(180deg); } |
| .dropdown-panel { |
| position: absolute; top: 100%; left: 0; padding-top: 6px; |
| } |
| .dropdown-panel::before { |
| content: ''; position: absolute; top: 6px; left: 0; right: 0; bottom: 0; |
| background: white; border-radius: 12px; |
| box-shadow: 0 10px 40px rgba(0,0,0,0.1), 0 2px 10px rgba(0,0,0,0.06); |
| border: 1px solid rgba(0,0,0,0.05); |
| } |
| .dd-item { |
| position: relative; display: flex; align-items: center; gap: 8px; |
| padding: 10px 14px; margin: 8px; border-radius: 8px; |
| font-size: 13px; font-weight: 500; color: var(--text-body); |
| text-decoration: none; transition: all 0.15s; cursor: pointer; min-width: 160px; |
| } |
| .dd-item:first-child { margin-top: 12px; } |
| .dd-item:last-child { margin-bottom: 12px; } |
| .dd-item:hover { background: var(--green-light); color: var(--green); } |
| |
| .nav-right { display: flex; align-items: center; gap: 8px; } |
| .nav-account { text-align: right; } |
| .account-label { display: block; font-size: 10px; color: var(--text-light); font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } |
| .account-value { font-size: 14px; font-weight: 700; color: var(--text-dark); } |
| .nav-divider { width: 1px; height: 28px; background: var(--border); margin: 0 8px; } |
| </style> |
| |