653472def3
对齐 VeighNa 4.4 原生 Qt UI,新增成交监控、资金监控、网关管理、全局配置等页面与 API,功能对等性 98.5%。 - 新增 API: /api/v1/trades, /api/v1/accounts, /api/v1/settings, 网关扩展 - 新增前端页面: 成交、资金、合约、网关、全局配置、微信通知 - 扩展导航菜单与实时数据推送 - 补充需求分析与实现计划文档
1086 lines
63 KiB
HTML
1086 lines
63 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Sanguo VeighNa Web</title>
|
|
<link rel="stylesheet" href="/static/css/main.css">
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- 登录界面 -->
|
|
<div v-if="!isLoggedIn" class="login-container">
|
|
<div class="login-box">
|
|
<h1>Sanguo VeighNa</h1>
|
|
<p class="subtitle">量化交易平台</p>
|
|
<form @submit.prevent="login" class="login-form">
|
|
<div class="form-group">
|
|
<label>用户名</label>
|
|
<input
|
|
type="text"
|
|
v-model="loginForm.username"
|
|
placeholder="请输入用户名"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>密码</label>
|
|
<input
|
|
type="password"
|
|
v-model="loginForm.password"
|
|
placeholder="请输入密码"
|
|
required
|
|
>
|
|
</div>
|
|
<div v-if="loginError" class="error-message">{{ loginError }}</div>
|
|
<button type="submit" class="btn-primary" :disabled="isLoading">
|
|
{{ isLoading ? '登录中...' : '登录' }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 主界面 -->
|
|
<div v-else class="main-container">
|
|
<!-- 侧边栏 -->
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h2>Sanguo VeighNa</h2>
|
|
<span class="version">v1.0.0</span>
|
|
</div>
|
|
<nav class="sidebar-nav">
|
|
<a
|
|
v-for="item in navItems"
|
|
:key="item.id"
|
|
href="#"
|
|
:class="{ active: currentPage === item.id }"
|
|
@click.prevent="currentPage = item.id"
|
|
>
|
|
<span class="icon">{{ item.icon }}</span>
|
|
<span>{{ item.label }}</span>
|
|
</a>
|
|
</nav>
|
|
<div class="sidebar-footer">
|
|
<div class="user-info">
|
|
<span>{{ currentUser.username }}</span>
|
|
</div>
|
|
<button @click="logout" class="btn-logout">退出登录</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- 内容区 -->
|
|
<main class="main-content">
|
|
<!-- 顶部状态栏 -->
|
|
<header class="top-bar">
|
|
<h1>{{ currentPageTitle }}</h1>
|
|
<div class="status-indicators">
|
|
<div class="status-item">
|
|
<span :class="{ connected: wsConnected }">
|
|
{{ wsConnected ? 'WebSocket 已连接' : 'WebSocket 未连接' }}
|
|
</span>
|
|
</div>
|
|
<div class="status-item">
|
|
网关状态:
|
|
<span :class="{ online: gatewayStatus.online }">
|
|
{{ gatewayStatus.online ? '在线' : '离线' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 总览页面 -->
|
|
<section v-if="currentPage === 'dashboard'" class="page-content">
|
|
<div class="dashboard-grid">
|
|
<!-- 系统状态卡片 -->
|
|
<div class="card">
|
|
<h3>系统状态</h3>
|
|
<div class="status-list">
|
|
<div class="status-row">
|
|
<span>VeighNa 服务:</span>
|
|
<span :class="{ online: systemStatus.vn_ready }">
|
|
{{ systemStatus.vn_ready ? '就绪' : '未就绪' }}
|
|
</span>
|
|
</div>
|
|
<div class="status-row">
|
|
<span>主引擎:</span>
|
|
<span :class="{ online: systemStatus.main_engine }">
|
|
{{ systemStatus.main_engine ? '运行中' : '停止' }}
|
|
</span>
|
|
</div>
|
|
<div class="status-row">
|
|
<span>交易引擎:</span>
|
|
<span :class="{ online: systemStatus.trading_engine }">
|
|
{{ systemStatus.trading_engine ? '运行中' : '停止' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 账户信息卡片 -->
|
|
<div class="card">
|
|
<h3>账户信息</h3>
|
|
<div v-if="accounts.length > 0" class="account-info">
|
|
<div class="account-row">
|
|
<span>账户ID:</span>
|
|
<span>{{ accounts[0].account_id }}</span>
|
|
</div>
|
|
<div class="account-row">
|
|
<span>余额:</span>
|
|
<span class="amount">{{ formatNumber(accounts[0].balance) }}</span>
|
|
</div>
|
|
<div class="account-row">
|
|
<span>可用:</span>
|
|
<span class="amount">{{ formatNumber(accounts[0].available) }}</span>
|
|
</div>
|
|
<div class="account-row">
|
|
<span>冻结:</span>
|
|
<span class="amount">{{ formatNumber(accounts[0].frozen) }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-else class="no-data">暂无账户数据</div>
|
|
</div>
|
|
|
|
<!-- 持仓汇总卡片 -->
|
|
<div class="card">
|
|
<h3>持仓汇总</h3>
|
|
<div v-if="positions.length > 0" class="position-summary">
|
|
<div class="summary-row">
|
|
<span>持仓品种:</span>
|
|
<span>{{ positions.length }}</span>
|
|
</div>
|
|
<div class="summary-row">
|
|
<span>总盈亏:</span>
|
|
<span :class="getPnLClass(totalPnL)">
|
|
{{ formatNumber(totalPnL) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div v-else class="no-data">暂无持仓</div>
|
|
</div>
|
|
|
|
<!-- 活动订单卡片 -->
|
|
<div class="card">
|
|
<h3>活动订单</h3>
|
|
<div v-if="activeOrders.length > 0" class="orders-summary">
|
|
<div class="summary-row">
|
|
<span>活动订单数:</span>
|
|
<span>{{ activeOrders.length }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-else class="no-data">暂无活动订单</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 系统日志 -->
|
|
<div class="card logs-card">
|
|
<h3>最新日志</h3>
|
|
<div class="logs-container">
|
|
<div
|
|
v-for="(log, index) in recentLogs"
|
|
:key="index"
|
|
:class="['log-entry', 'log-' + log.level]"
|
|
>
|
|
<span class="log-time">{{ log.time }}</span>
|
|
<span class="log-level">{{ log.level }}</span>
|
|
<span class="log-message">{{ log.message }}</span>
|
|
</div>
|
|
<div v-if="recentLogs.length === 0" class="no-data">暂无日志</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 行情页面 -->
|
|
<section v-if="currentPage === 'market'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>行情数据</h3>
|
|
<div class="card-actions">
|
|
<input
|
|
type="text"
|
|
v-model="marketSearch"
|
|
placeholder="搜索合约..."
|
|
class="search-input"
|
|
>
|
|
<button @click="exportTable('market')" class="btn-secondary">导出CSV</button>
|
|
<button @click="refreshMarket" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th @click="sortTable('vt_symbol')" style="cursor: pointer">
|
|
合约 {{ getSortIcon('vt_symbol') }}
|
|
</th>
|
|
<th @click="sortTable('last_price')" style="cursor: pointer">
|
|
最新价 {{ getSortIcon('last_price') }}
|
|
</th>
|
|
<th @click="sortTable('bid_price_1')" style="cursor: pointer">
|
|
买价 {{ getSortIcon('bid_price_1') }}
|
|
</th>
|
|
<th @click="sortTable('ask_price_1')" style="cursor: pointer">
|
|
卖价 {{ getSortIcon('ask_price_1') }}
|
|
</th>
|
|
<th @click="sortTable('volume')" style="cursor: pointer">
|
|
成交量 {{ getSortIcon('volume') }}
|
|
</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="tick in sortData(filteredTicks, tableSort.field, tableSort.order)"
|
|
:key="tick.vt_symbol"
|
|
@click="selectTick(tick)"
|
|
@dblclick="handleTickDoubleClick(tick)"
|
|
style="cursor: pointer"
|
|
:class="{ 'selected-row': selectedTick && selectedTick.vt_symbol === tick.vt_symbol }"
|
|
>
|
|
<td>{{ tick.vt_symbol }}</td>
|
|
<td :class="getPriceClass(tick.last_price, tick.pre_close_price)">
|
|
{{ formatNumber(tick.last_price) }}
|
|
</td>
|
|
<td class="bid-cell">{{ formatNumber(tick.bid_price_1) }}</td>
|
|
<td class="ask-cell">{{ formatNumber(tick.ask_price_1) }}</td>
|
|
<td>{{ tick.volume }}</td>
|
|
<td @click.stop>
|
|
<button
|
|
@click="subscribeSymbol(tick.vt_symbol)"
|
|
class="btn-small"
|
|
>
|
|
{{ isSubscribed(tick.vt_symbol) ? '已订阅' : '订阅' }}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="filteredTicks.length === 0" class="no-data">暂无行情数据</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 交易页面 -->
|
|
<section v-if="currentPage === 'trading'" class="page-content">
|
|
<div class="trading-grid">
|
|
<!-- 下单表单 -->
|
|
<div class="card">
|
|
<h3>下单</h3>
|
|
<form @submit.prevent="sendOrder" class="order-form">
|
|
<div class="form-group">
|
|
<label>合约</label>
|
|
<select v-model="orderForm.symbol" required>
|
|
<option value="">请选择合约</option>
|
|
<option
|
|
v-for="contract in contracts"
|
|
:key="contract.vt_symbol"
|
|
:value="contract.vt_symbol"
|
|
>
|
|
{{ contract.vt_symbol }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>方向</label>
|
|
<select v-model="orderForm.direction" required>
|
|
<option value="buy">买入</option>
|
|
<option value="sell">卖出</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>类型</label>
|
|
<select v-model="orderForm.order_type" required>
|
|
<option value="limit">限价</option>
|
|
<option value="market">市价</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>价格</label>
|
|
<input
|
|
type="number"
|
|
v-model.number="orderForm.price"
|
|
step="0.01"
|
|
:disabled="orderForm.order_type === 'market'"
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>数量</label>
|
|
<input type="number" v-model.number="orderForm.volume" required min="1">
|
|
</div>
|
|
<div v-if="orderError" class="error-message">{{ orderError }}</div>
|
|
<button type="submit" class="btn-primary" :disabled="isOrdering">
|
|
{{ isOrdering ? '下单中...' : '下单' }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 市场深度盘口 -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>市场深度</h3>
|
|
<span v-if="selectedTick" class="selected-tick">{{ selectedTick.vt_symbol }}</span>
|
|
</div>
|
|
<div v-if="selectedTick" class="order-book">
|
|
<!-- 卖盘 -->
|
|
<div class="order-book-ask">
|
|
<div class="order-book-row ask">
|
|
<span class="price">{{ formatNumber(selectedTick?.ask_price_5) }}</span>
|
|
<span class="volume">{{ selectedTick?.ask_volume_5 }}</span>
|
|
</div>
|
|
<div class="order-book-row ask">
|
|
<span class="price">{{ formatNumber(selectedTick?.ask_price_4) }}</span>
|
|
<span class="volume">{{ selectedTick?.ask_volume_4 }}</span>
|
|
</div>
|
|
<div class="order-book-row ask">
|
|
<span class="price">{{ formatNumber(selectedTick?.ask_price_3) }}</span>
|
|
<span class="volume">{{ selectedTick?.ask_volume_3 }}</span>
|
|
</div>
|
|
<div class="order-book-row ask">
|
|
<span class="price">{{ formatNumber(selectedTick?.ask_price_2) }}</span>
|
|
<span class="volume">{{ selectedTick?.ask_volume_2 }}</span>
|
|
</div>
|
|
<div class="order-book-row ask">
|
|
<span class="price">{{ formatNumber(selectedTick?.ask_price_1) }}</span>
|
|
<span class="volume">{{ selectedTick?.ask_volume_1 }}</span>
|
|
</div>
|
|
</div>
|
|
<!-- 中间行:最新价 -->
|
|
<div class="order-book-middle">
|
|
<span class="last-price">{{ formatNumber(selectedTick?.last_price) }}</span>
|
|
<span v-if="selectedTick?.pre_close_price" class="price-change" :class="getPriceClass(selectedTick?.last_price, selectedTick?.pre_close_price)">
|
|
{{ ((selectedTick?.last_price - selectedTick?.pre_close_price) / selectedTick?.pre_close_price * 100).toFixed(2) }}%
|
|
</span>
|
|
</div>
|
|
<!-- 买盘 -->
|
|
<div class="order-book-bid">
|
|
<div class="order-book-row bid">
|
|
<span class="price">{{ formatNumber(selectedTick?.bid_price_1) }}</span>
|
|
<span class="volume">{{ selectedTick?.bid_volume_1 }}</span>
|
|
</div>
|
|
<div class="order-book-row bid">
|
|
<span class="price">{{ formatNumber(selectedTick?.bid_price_2) }}</span>
|
|
<span class="volume">{{ selectedTick?.bid_volume_2 }}</span>
|
|
</div>
|
|
<div class="order-book-row bid">
|
|
<span class="price">{{ formatNumber(selectedTick?.bid_price_3) }}</span>
|
|
<span class="volume">{{ selectedTick?.bid_volume_3 }}</span>
|
|
</div>
|
|
<div class="order-book-row bid">
|
|
<span class="price">{{ formatNumber(selectedTick?.bid_price_4) }}</span>
|
|
<span class="volume">{{ selectedTick?.bid_volume_4 }}</span>
|
|
</div>
|
|
<div class="order-book-row bid">
|
|
<span class="price">{{ formatNumber(selectedTick?.bid_price_5) }}</span>
|
|
<span class="volume">{{ selectedTick?.bid_volume_5 }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="no-data">
|
|
请在行情页面点击选择合约查看市场深度
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 委托列表 -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>委托列表</h3>
|
|
<div class="card-actions">
|
|
<button @click="exportTable('orders')" class="btn-secondary">导出CSV</button>
|
|
<button @click="refreshOrders" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th @click="sortTable('order_id')" style="cursor: pointer">
|
|
订单ID {{ getSortIcon('order_id') }}
|
|
</th>
|
|
<th @click="sortTable('symbol')" style="cursor: pointer">
|
|
合约 {{ getSortIcon('symbol') }}
|
|
</th>
|
|
<th @click="sortTable('direction')" style="cursor: pointer">
|
|
方向 {{ getSortIcon('direction') }}
|
|
</th>
|
|
<th @click="sortTable('price')" style="cursor: pointer">
|
|
价格 {{ getSortIcon('price') }}
|
|
</th>
|
|
<th @click="sortTable('volume')" style="cursor: pointer">
|
|
数量 {{ getSortIcon('volume') }}
|
|
</th>
|
|
<th @click="sortTable('traded')" style="cursor: pointer">
|
|
成交 {{ getSortIcon('traded') }}
|
|
</th>
|
|
<th @click="sortTable('status')" style="cursor: pointer">
|
|
状态 {{ getSortIcon('status') }}
|
|
</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="order in sortData(allOrders, tableSort.field, tableSort.order)"
|
|
:key="order.order_id"
|
|
@dblclick="handleOrderDoubleClick(order)"
|
|
style="cursor: pointer"
|
|
>
|
|
<td>{{ order.order_id }}</td>
|
|
<td>{{ order.symbol }}</td>
|
|
<td :class="getDirectionClass(order.direction)">{{ order.direction }}</td>
|
|
<td>{{ formatNumber(order.price) }}</td>
|
|
<td>{{ order.volume }}</td>
|
|
<td>{{ order.traded }}</td>
|
|
<td>{{ order.status }}</td>
|
|
<td @click.stop>
|
|
<button
|
|
v-if="canCancel(order.status)"
|
|
@click="cancelOrder(order.order_id)"
|
|
class="btn-small btn-danger"
|
|
>
|
|
撤单
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="allOrders.length === 0" class="no-data">暂无委托</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 持仓页面 -->
|
|
<section v-if="currentPage === 'position'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>持仓列表</h3>
|
|
<div class="card-actions">
|
|
<button @click="exportTable('positions')" class="btn-secondary">导出CSV</button>
|
|
<button @click="refreshPositions" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th @click="sortTable('symbol')" style="cursor: pointer">
|
|
合约 {{ getSortIcon('symbol') }}
|
|
</th>
|
|
<th @click="sortTable('exchange')" style="cursor: pointer">
|
|
交易所 {{ getSortIcon('exchange') }}
|
|
</th>
|
|
<th @click="sortTable('direction')" style="cursor: pointer">
|
|
方向 {{ getSortIcon('direction') }}
|
|
</th>
|
|
<th @click="sortTable('volume')" style="cursor: pointer">
|
|
数量 {{ getSortIcon('volume') }}
|
|
</th>
|
|
<th @click="sortTable('available')" style="cursor: pointer">
|
|
可用 {{ getSortIcon('available') }}
|
|
</th>
|
|
<th @click="sortTable('price')" style="cursor: pointer">
|
|
均价 {{ getSortIcon('price') }}
|
|
</th>
|
|
<th @click="sortTable('pnl')" style="cursor: pointer">
|
|
盈亏 {{ getSortIcon('pnl') }}
|
|
</th>
|
|
<th @click="sortTable('pnl_ratio')" style="cursor: pointer">
|
|
盈亏率 {{ getSortIcon('pnl_ratio') }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="pos in sortData(positions, tableSort.field, tableSort.order)"
|
|
:key="pos.vt_symbol"
|
|
@dblclick="handlePositionDoubleClick(pos)"
|
|
style="cursor: pointer"
|
|
>
|
|
<td>{{ pos.symbol }}</td>
|
|
<td>{{ pos.exchange }}</td>
|
|
<td :class="getDirectionClass(pos.direction)">{{ pos.direction }}</td>
|
|
<td>{{ pos.volume }}</td>
|
|
<td>{{ pos.volume - (pos.frozen || 0) }}</td>
|
|
<td>{{ formatNumber(pos.price) }}</td>
|
|
<td :class="getPnLClass(pos.pnl)">
|
|
{{ formatNumber(pos.pnl) }}
|
|
</td>
|
|
<td :class="getPnLClass(pos.pnl)">
|
|
{{ formatPercent(pos.pnl_ratio) }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="positions.length === 0" class="no-data">暂无持仓</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 活动委托视图页面 -->
|
|
<section v-if="currentPage === 'active_orders'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>活动委托</h3>
|
|
<div class="card-actions">
|
|
<span class="info-text">只显示未成交和部分成交的订单</span>
|
|
<button @click="exportTable('active_orders')" class="btn-secondary">导出CSV</button>
|
|
<button @click="refreshOrders" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th @click="sortTable('order_id')" style="cursor: pointer">
|
|
订单ID {{ getSortIcon('order_id') }}
|
|
</th>
|
|
<th @click="sortTable('symbol')" style="cursor: pointer">
|
|
合约 {{ getSortIcon('symbol') }}
|
|
</th>
|
|
<th @click="sortTable('direction')" style="cursor: pointer">
|
|
方向 {{ getSortIcon('direction') }}
|
|
</th>
|
|
<th @click="sortTable('price')" style="cursor: pointer">
|
|
价格 {{ getSortIcon('price') }}
|
|
</th>
|
|
<th @click="sortTable('volume')" style="cursor: pointer">
|
|
数量 {{ getSortIcon('volume') }}
|
|
</th>
|
|
<th @click="sortTable('traded')" style="cursor: pointer">
|
|
已成交 {{ getSortIcon('traded') }}
|
|
</th>
|
|
<th @click="sortTable('status')" style="cursor: pointer">
|
|
状态 {{ getSortIcon('status') }}
|
|
</th>
|
|
<th @click="sortTable('time')" style="cursor: pointer">
|
|
时间 {{ getSortIcon('time') }}
|
|
</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="order in sortData(activeOrders, tableSort.field, tableSort.order)"
|
|
:key="order.order_id"
|
|
@dblclick="handleOrderDoubleClick(order)"
|
|
style="cursor: pointer"
|
|
>
|
|
<td>{{ order.order_id }}</td>
|
|
<td>{{ order.symbol }}</td>
|
|
<td :class="getDirectionClass(order.direction)">{{ order.direction }}</td>
|
|
<td>{{ formatNumber(order.price) }}</td>
|
|
<td>{{ order.volume }}</td>
|
|
<td>{{ order.traded }}</td>
|
|
<td>{{ order.status }}</td>
|
|
<td>{{ formatTime(order.time) }}</td>
|
|
<td @click.stop>
|
|
<button
|
|
@click="cancelOrder(order.order_id)"
|
|
class="btn-small btn-danger"
|
|
>
|
|
撤单
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="activeOrders.length === 0" class="no-data">暂无活动委托</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 成交监控页面 -->
|
|
<section v-if="currentPage === 'strategy'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>策略列表</h3>
|
|
<button @click="refreshStrategies" class="btn-secondary">刷新</button>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th @click="sortTable('strategy_name')" style="cursor: pointer">
|
|
策略名称 {{ getSortIcon('strategy_name') }}
|
|
</th>
|
|
<th @click="sortTable('class_name')" style="cursor: pointer">
|
|
类名 {{ getSortIcon('class_name') }}
|
|
</th>
|
|
<th @click="sortTable('status')" style="cursor: pointer">
|
|
状态 {{ getSortIcon('status') }}
|
|
</th>
|
|
<th @click="sortTable('created_at')" style="cursor: pointer">
|
|
创建时间 {{ getSortIcon('created_at') }}
|
|
</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="strategy in sortData(strategies, tableSort.field, tableSort.order)" :key="strategy.strategy_name">
|
|
<td>{{ strategy.strategy_name }}</td>
|
|
<td>{{ strategy.class_name }}</td>
|
|
<td>
|
|
<span :class="'status-' + strategy.status.toLowerCase()">
|
|
{{ strategy.status }}
|
|
</span>
|
|
</td>
|
|
<td>{{ formatTime(strategy.created_at) }}</td>
|
|
<td>
|
|
<button
|
|
v-if="strategy.status === 'Stopped'"
|
|
@click="startStrategy(strategy.strategy_name)"
|
|
class="btn-small"
|
|
>
|
|
启动
|
|
</button>
|
|
<button
|
|
v-if="strategy.status === 'Running'"
|
|
@click="stopStrategy(strategy.strategy_name)"
|
|
class="btn-small btn-danger"
|
|
>
|
|
停止
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="strategies.length === 0" class="no-data">暂无策略</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 成交监控页面 -->
|
|
<section v-if="currentPage === 'trades'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>成交记录</h3>
|
|
<div class="card-actions">
|
|
<button @click="exportTable('trades')" class="btn-secondary">导出CSV</button>
|
|
<button @click="refreshTrades" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th @click="sortTable('tradeid')" style="cursor: pointer">
|
|
成交号 {{ getSortIcon('tradeid') }}
|
|
</th>
|
|
<th @click="sortTable('orderid')" style="cursor: pointer">
|
|
委托号 {{ getSortIcon('orderid') }}
|
|
</th>
|
|
<th @click="sortTable('symbol')" style="cursor: pointer">
|
|
合约 {{ getSortIcon('symbol') }}
|
|
</th>
|
|
<th @click="sortTable('direction')" style="cursor: pointer">
|
|
方向 {{ getSortIcon('direction') }}
|
|
</th>
|
|
<th @click="sortTable('offset')" style="cursor: pointer">
|
|
开平 {{ getSortIcon('offset') }}
|
|
</th>
|
|
<th @click="sortTable('price')" style="cursor: pointer">
|
|
价格 {{ getSortIcon('price') }}
|
|
</th>
|
|
<th @click="sortTable('volume')" style="cursor: pointer">
|
|
数量 {{ getSortIcon('volume') }}
|
|
</th>
|
|
<th @click="sortTable('datetime')" style="cursor: pointer">
|
|
时间 {{ getSortIcon('datetime') }}
|
|
</th>
|
|
<th>接口</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="trade in sortData(trades, tableSort.field, tableSort.order)" :key="trade.tradeid">
|
|
<td>{{ trade.tradeid }}</td>
|
|
<td>{{ trade.orderid }}</td>
|
|
<td>{{ trade.symbol }}</td>
|
|
<td :class="getDirectionClass(trade.direction)">{{ trade.direction }}</td>
|
|
<td>{{ trade.offset }}</td>
|
|
<td>{{ formatNumber(trade.price) }}</td>
|
|
<td>{{ trade.volume }}</td>
|
|
<td>{{ trade.datetime }}</td>
|
|
<td>{{ trade.gateway_name }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="trades.length === 0" class="no-data">暂无成交记录</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 资金监控页面 -->
|
|
<section v-if="currentPage === 'account'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>资金监控</h3>
|
|
<div class="card-actions">
|
|
<button @click="exportTable('accounts')" class="btn-secondary">导出CSV</button>
|
|
<button @click="refreshAllAccounts" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th @click="sortTable('accountid')" style="cursor: pointer">
|
|
账号 {{ getSortIcon('accountid') }}
|
|
</th>
|
|
<th @click="sortTable('balance')" style="cursor: pointer">
|
|
余额 {{ getSortIcon('balance') }}
|
|
</th>
|
|
<th @click="sortTable('frozen')" style="cursor: pointer">
|
|
冻结 {{ getSortIcon('frozen') }}
|
|
</th>
|
|
<th @click="sortTable('available')" style="cursor: pointer">
|
|
可用 {{ getSortIcon('available') }}
|
|
</th>
|
|
<th>接口</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="account in sortData(allAccounts, tableSort.field, tableSort.order)" :key="account.accountid">
|
|
<td>{{ account.accountid }}</td>
|
|
<td class="amount">{{ formatNumber(account.balance) }}</td>
|
|
<td class="amount">{{ formatNumber(account.frozen) }}</td>
|
|
<td class="amount">{{ formatNumber(account.available) }}</td>
|
|
<td>{{ account.gateway_name }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="allAccounts.length === 0" class="no-data">暂无账户数据</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 合约管理页面 -->
|
|
<section v-if="currentPage === 'contracts'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>合约查询</h3>
|
|
<div class="card-actions">
|
|
<input
|
|
type="text"
|
|
v-model="contractSearch"
|
|
placeholder="搜索合约代码、名称或交易所..."
|
|
class="search-input"
|
|
>
|
|
<button @click="exportTable('contracts')" class="btn-secondary">导出CSV</button>
|
|
<button @click="refreshContracts" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th @click="sortTable('vt_symbol')" style="cursor: pointer">
|
|
本地代码 {{ getSortIcon('vt_symbol') }}
|
|
</th>
|
|
<th @click="sortTable('symbol')" style="cursor: pointer">
|
|
代码 {{ getSortIcon('symbol') }}
|
|
</th>
|
|
<th @click="sortTable('exchange')" style="cursor: pointer">
|
|
交易所 {{ getSortIcon('exchange') }}
|
|
</th>
|
|
<th @click="sortTable('name')" style="cursor: pointer">
|
|
名称 {{ getSortIcon('name') }}
|
|
</th>
|
|
<th @click="sortTable('product')" style="cursor: pointer">
|
|
分类 {{ getSortIcon('product') }}
|
|
</th>
|
|
<th @click="sortTable('size')" style="cursor: pointer">
|
|
乘数 {{ getSortIcon('size') }}
|
|
</th>
|
|
<th @click="sortTable('pricetick')" style="cursor: pointer">
|
|
跳动点 {{ getSortIcon('pricetick') }}
|
|
</th>
|
|
<th @click="sortTable('min_volume')" style="cursor: pointer">
|
|
最小量 {{ getSortIcon('min_volume') }}
|
|
</th>
|
|
<th>期权类型</th>
|
|
<th>到期日</th>
|
|
<th>行权价</th>
|
|
<th>接口</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="contract in sortData(filteredContracts, tableSort.field, tableSort.order)" :key="contract.vt_symbol">
|
|
<td>{{ contract.vt_symbol }}</td>
|
|
<td>{{ contract.symbol }}</td>
|
|
<td>{{ contract.exchange }}</td>
|
|
<td>{{ contract.name }}</td>
|
|
<td>{{ contract.product }}</td>
|
|
<td>{{ contract.size }}</td>
|
|
<td>{{ contract.pricetick }}</td>
|
|
<td>{{ contract.min_volume }}</td>
|
|
<td>{{ contract.option_type || '-' }}</td>
|
|
<td>{{ contract.option_expiry || '-' }}</td>
|
|
<td>{{ contract.option_strike || '-' }}</td>
|
|
<td>{{ contract.gateway_name }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="filteredContracts.length === 0" class="no-data">暂无合约数据</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 网关管理页面 -->
|
|
<section v-if="currentPage === 'gateway'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>网关管理</h3>
|
|
<button @click="refreshGateways" class="btn-secondary">刷新</button>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>网关名称</th>
|
|
<th>类型</th>
|
|
<th>状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="gateway in gateways" :key="gateway.name">
|
|
<td>{{ gateway.name }}</td>
|
|
<td>{{ gateway.type || gateway.gateway_name }}</td>
|
|
<td>
|
|
<span :class="getGatewayStatusClass(gateway)">
|
|
{{ getGatewayStatusText(gateway) }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<button
|
|
v-if="!gateway.connected"
|
|
@click="openConnectDialog(gateway.name)"
|
|
class="btn-small"
|
|
>
|
|
连接
|
|
</button>
|
|
<button
|
|
v-if="gateway.connected"
|
|
@click="disconnectGateway(gateway.name)"
|
|
class="btn-small btn-danger"
|
|
>
|
|
断开
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="gateways.length === 0" class="no-data">暂无可用网关</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 网关连接对话框 -->
|
|
<div v-if="showConnectDialog" class="modal-overlay" @click.self="closeConnectDialog">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3>连接 {{ selectedGateway }}</h3>
|
|
<button @click="closeConnectDialog" class="modal-close">×</button>
|
|
</div>
|
|
<form @submit.prevent="connectGateway" class="modal-body">
|
|
<div
|
|
v-for="(value, key) in gatewayForm"
|
|
:key="key"
|
|
class="form-group"
|
|
>
|
|
<label>{{ key }}</label>
|
|
<input
|
|
v-if="typeof value === 'string' && !isFormFieldPassword(key)"
|
|
type="text"
|
|
v-model="gatewayForm[key]"
|
|
>
|
|
<input
|
|
v-if="typeof value === 'string' && isFormFieldPassword(key)"
|
|
type="password"
|
|
v-model="gatewayForm[key]"
|
|
>
|
|
<input
|
|
v-else-if="typeof value === 'number'"
|
|
type="number"
|
|
v-model.number="gatewayForm[key]"
|
|
step="any"
|
|
>
|
|
<select
|
|
v-else-if="Array.isArray(value)"
|
|
v-model="gatewayForm[key]"
|
|
>
|
|
<option
|
|
v-for="option in value"
|
|
:key="option"
|
|
:value="option"
|
|
>
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" @click="closeConnectDialog" class="btn-secondary">取消</button>
|
|
<button type="submit" class="btn-primary" :disabled="isConnecting">
|
|
{{ isConnecting ? '连接中...' : '连接' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 全局配置页面 -->
|
|
<section v-if="currentPage === 'global_settings'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>全局配置</h3>
|
|
<div class="card-actions">
|
|
<span class="info-text">配置修改需要重启后才会生效</span>
|
|
</div>
|
|
</div>
|
|
<div class="global-settings-container">
|
|
<div v-if="globalSettingsLoading" class="loading">加载中...</div>
|
|
<div v-else-if="globalSettingsError" class="error-message">{{ globalSettingsError }}</div>
|
|
<div v-else class="settings-grid">
|
|
<div
|
|
v-for="(value, key) in globalSettings"
|
|
:key="key"
|
|
class="setting-item"
|
|
>
|
|
<div class="setting-label">
|
|
<span class="setting-key">{{ key }}</span>
|
|
<span class="setting-type">({{ getSettingType(value) }})</span>
|
|
</div>
|
|
<div class="setting-value">
|
|
<input
|
|
v-if="typeof value === 'string'"
|
|
type="text"
|
|
v-model="globalSettings[key]"
|
|
class="setting-input"
|
|
>
|
|
<input
|
|
v-else-if="typeof value === 'number'"
|
|
type="number"
|
|
v-model.number="globalSettings[key]"
|
|
class="setting-input"
|
|
>
|
|
<input
|
|
v-else-if="typeof value === 'boolean'"
|
|
type="checkbox"
|
|
v-model="globalSettings[key]"
|
|
class="setting-checkbox"
|
|
>
|
|
<select
|
|
v-else-if="Array.isArray(value)"
|
|
v-model="globalSettings[key]"
|
|
class="setting-select"
|
|
>
|
|
<option
|
|
v-for="option in value"
|
|
:key="option"
|
|
:value="option"
|
|
>
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
<input
|
|
v-else
|
|
type="text"
|
|
:value="String(value)"
|
|
class="setting-input"
|
|
disabled
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="settings-actions">
|
|
<button @click="saveGlobalSettings" class="btn-primary" :disabled="isSavingSettings">
|
|
{{ isSavingSettings ? '保存中...' : '保存配置' }}
|
|
</button>
|
|
<button @click="refreshGlobalSettings" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 微信通知设置页面 -->
|
|
<section v-if="currentPage === 'wechat'" class="page-content">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>微信通知</h3>
|
|
<div class="card-actions">
|
|
<span class="info-text">功能开发中,敬请期待</span>
|
|
</div>
|
|
</div>
|
|
<div class="wechat-container">
|
|
<div class="wechat-status">
|
|
<div class="status-row">
|
|
<span>绑定状态:</span>
|
|
<span class="status-badge status-pending">未绑定</span>
|
|
</div>
|
|
<div class="status-row">
|
|
<span>推送间隔:</span>
|
|
<span>60 秒</span>
|
|
</div>
|
|
</div>
|
|
<div class="wechat-actions">
|
|
<button class="btn-primary" disabled>绑定微信</button>
|
|
<button class="btn-secondary" disabled>测试消息</button>
|
|
<button class="btn-danger" disabled>解除绑定</button>
|
|
</div>
|
|
<div class="wechat-info">
|
|
<p>微信通知功能正在开发中,将支持以下特性:</p>
|
|
<ul>
|
|
<li>扫码绑定微信账号</li>
|
|
<li>交易信号实时推送</li>
|
|
<li>账户变化通知</li>
|
|
<li>自定义推送间隔</li>
|
|
<li>测试消息功能</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 日志页面 -->
|
|
<section v-if="currentPage === 'log'" class="page-content">
|
|
<div class="card logs-card">
|
|
<div class="card-header">
|
|
<h3>系统日志</h3>
|
|
<div class="card-actions">
|
|
<label>
|
|
<input type="checkbox" v-model="autoScroll"> 自动滚动
|
|
</label>
|
|
<select v-model="logLevelFilter">
|
|
<option value="">全部</option>
|
|
<option value="DEBUG">DEBUG</option>
|
|
<option value="INFO">INFO</option>
|
|
<option value="WARNING">WARNING</option>
|
|
<option value="ERROR">ERROR</option>
|
|
</select>
|
|
<button @click="clearLogs" class="btn-secondary">清空</button>
|
|
</div>
|
|
</div>
|
|
<div class="logs-container" ref="logsContainer">
|
|
<div
|
|
v-for="(log, index) in filteredLogs"
|
|
:key="index"
|
|
:class="['log-entry', 'log-' + log.level.toLowerCase()]"
|
|
>
|
|
<span class="log-time">{{ formatTime(log.time) }}</span>
|
|
<span class="log-level">{{ log.level }}</span>
|
|
<span class="log-message">{{ log.message }}</span>
|
|
</div>
|
|
<div v-if="filteredLogs.length === 0" class="no-data">暂无日志</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/js/api.js"></script>
|
|
<script src="/static/js/websocket.js"></script>
|
|
<script src="/static/js/app.js"></script>
|
|
</body>
|
|
</html>
|