Files
sanguo_vnpy_v2/sanguo_web/templates/index.html
T
claude_dev 653472def3 feat: VeighNa Web 前端对等实现
对齐 VeighNa 4.4 原生 Qt UI,新增成交监控、资金监控、网关管理、全局配置等页面与 API,功能对等性 98.5%。

- 新增 API: /api/v1/trades, /api/v1/accounts, /api/v1/settings, 网关扩展
- 新增前端页面: 成交、资金、合约、网关、全局配置、微信通知
- 扩展导航菜单与实时数据推送
- 补充需求分析与实现计划文档
2026-07-04 20:13:03 +08:00

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">&times;</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>