918bbed0fc
- 修复 deps.py 中 get_vn_service 的引用错误 (vn_service.vn_service -> vn_service) - 移除登录页面上的明文密码提示 - 改进前端错误处理,避免数据加载失败导致登录显示错误
483 lines
25 KiB
HTML
483 lines
25 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="refreshMarket" class="btn-secondary">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>合约</th>
|
|
<th>最新价</th>
|
|
<th>买价</th>
|
|
<th>卖价</th>
|
|
<th>成交量</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="tick in filteredTicks" :key="tick.vt_symbol">
|
|
<td>{{ tick.vt_symbol }}</td>
|
|
<td :class="getPriceClass(tick.last_price, tick.pre_close_price)">
|
|
{{ formatNumber(tick.last_price) }}
|
|
</td>
|
|
<td>{{ formatNumber(tick.bid_price_1) }}</td>
|
|
<td>{{ formatNumber(tick.ask_price_1) }}</td>
|
|
<td>{{ tick.volume }}</td>
|
|
<td>
|
|
<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>
|
|
<button @click="refreshOrders" class="btn-secondary">刷新</button>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>订单ID</th>
|
|
<th>合约</th>
|
|
<th>方向</th>
|
|
<th>价格</th>
|
|
<th>数量</th>
|
|
<th>成交</th>
|
|
<th>状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="order in allOrders" :key="order.order_id">
|
|
<td>{{ order.order_id }}</td>
|
|
<td>{{ order.symbol }}</td>
|
|
<td :class="order.direction">{{ order.direction }}</td>
|
|
<td>{{ formatNumber(order.price) }}</td>
|
|
<td>{{ order.volume }}</td>
|
|
<td>{{ order.traded }}</td>
|
|
<td>{{ order.status }}</td>
|
|
<td>
|
|
<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>
|
|
<button @click="refreshPositions" class="btn-secondary">刷新</button>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>合约</th>
|
|
<th>交易所</th>
|
|
<th>方向</th>
|
|
<th>数量</th>
|
|
<th>可用</th>
|
|
<th>均价</th>
|
|
<th>盈亏</th>
|
|
<th>盈亏率</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="pos in positions" :key="pos.vt_symbol">
|
|
<td>{{ pos.symbol }}</td>
|
|
<td>{{ pos.exchange }}</td>
|
|
<td :class="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 === '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>策略名称</th>
|
|
<th>类名</th>
|
|
<th>状态</th>
|
|
<th>创建时间</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="strategy in strategies" :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 === '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>
|