Files
sanguo_vnpy_v2/sanguo_web/templates/index.html
T
claude_dev 918bbed0fc fix: 修复登录500错误和移除明文密码提示
- 修复 deps.py 中 get_vn_service 的引用错误 (vn_service.vn_service -> vn_service)
- 移除登录页面上的明文密码提示
- 改进前端错误处理,避免数据加载失败导致登录显示错误
2026-07-02 12:23:55 +08:00

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>