918bbed0fc
- 修复 deps.py 中 get_vn_service 的引用错误 (vn_service.vn_service -> vn_service) - 移除登录页面上的明文密码提示 - 改进前端错误处理,避免数据加载失败导致登录显示错误
566 lines
17 KiB
HTML
566 lines
17 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>WebSocket 测试 - Sanguo VeighNa</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
background: #f5f7fa;
|
|
color: #333;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
color: #2c3e50;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.panel {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.panel h2 {
|
|
font-size: 18px;
|
|
color: #34495e;
|
|
margin-bottom: 15px;
|
|
border-bottom: 2px solid #ecf0f1;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.connection-controls {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
input[type="text"] {
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
button {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
button.primary {
|
|
background: #3498db;
|
|
color: white;
|
|
}
|
|
|
|
button.primary:hover {
|
|
background: #2980b9;
|
|
}
|
|
|
|
button.danger {
|
|
background: #e74c3c;
|
|
color: white;
|
|
}
|
|
|
|
button.danger:hover {
|
|
background: #c0392b;
|
|
}
|
|
|
|
button.success {
|
|
background: #2ecc71;
|
|
color: white;
|
|
}
|
|
|
|
button.success:hover {
|
|
background: #27ae60;
|
|
}
|
|
|
|
button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.status {
|
|
display: inline-block;
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
.status.connected {
|
|
background: #2ecc71;
|
|
color: white;
|
|
}
|
|
|
|
.status.disconnected {
|
|
background: #e74c3c;
|
|
color: white;
|
|
}
|
|
|
|
.subscription-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
gap: 10px;
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.checkbox-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.checkbox-item input[type="checkbox"] {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.symbol-input {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.symbol-input input {
|
|
flex: 1;
|
|
}
|
|
|
|
.log-container {
|
|
background: #2c3e50;
|
|
color: #ecf0f1;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
height: 400px;
|
|
overflow-y: auto;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 5px;
|
|
padding: 3px 0;
|
|
}
|
|
|
|
.log-entry.sent {
|
|
color: #3498db;
|
|
}
|
|
|
|
.log-entry.received {
|
|
color: #2ecc71;
|
|
}
|
|
|
|
.log-entry.error {
|
|
color: #e74c3c;
|
|
}
|
|
|
|
.log-entry.info {
|
|
color: #f39c12;
|
|
}
|
|
|
|
.log-timestamp {
|
|
color: #7f8c8d;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.message-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
|
gap: 10px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
padding: 10px;
|
|
background: #ecf0f1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: #7f8c8d;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>WebSocket 测试 - Sanguo VeighNa</h1>
|
|
|
|
<!-- Connection Panel -->
|
|
<div class="panel">
|
|
<h2>连接控制</h2>
|
|
<div class="connection-controls">
|
|
<input type="text" id="wsUrl" value="ws://localhost:8000/ws" placeholder="WebSocket URL">
|
|
<input type="text" id="token" placeholder="JWT Token (可选)">
|
|
<button id="connectBtn" class="primary">连接</button>
|
|
<button id="disconnectBtn" class="danger" disabled>断开</button>
|
|
<span id="status" class="status disconnected">未连接</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Subscription Panel -->
|
|
<div class="panel">
|
|
<h2>订阅管理</h2>
|
|
<div class="subscription-grid">
|
|
<label class="checkbox-item">
|
|
<input type="checkbox" id="sub-tick" value="tick">
|
|
<span>行情 (Tick)</span>
|
|
</label>
|
|
<label class="checkbox-item">
|
|
<input type="checkbox" id="sub-order" value="order">
|
|
<span>订单 (Order)</span>
|
|
</label>
|
|
<label class="checkbox-item">
|
|
<input type="checkbox" id="sub-trade" value="trade">
|
|
<span>成交 (Trade)</span>
|
|
</label>
|
|
<label class="checkbox-item">
|
|
<input type="checkbox" id="sub-position" value="position">
|
|
<span>持仓 (Position)</span>
|
|
</label>
|
|
<label class="checkbox-item">
|
|
<input type="checkbox" id="sub-account" value="account">
|
|
<span>账户 (Account)</span>
|
|
</label>
|
|
<label class="checkbox-item">
|
|
<input type="checkbox" id="sub-log" value="log">
|
|
<span>日志 (Log)</span>
|
|
</label>
|
|
<label class="checkbox-item">
|
|
<input type="checkbox" id="sub-contract" value="contract">
|
|
<span>合约 (Contract)</span>
|
|
</label>
|
|
</div>
|
|
<button id="subscribeBtn" class="success" disabled>订阅选中</button>
|
|
<button id="unsubscribeBtn" class="danger" disabled>取消选中</button>
|
|
|
|
<div class="symbol-input">
|
|
<input type="text" id="symbolInput" placeholder="品种代码 (逗号分隔,如: IF2501.CFFEX,IH2501.CFFEX)">
|
|
<button id="subscribeSymbolBtn" class="success" disabled>订阅品种</button>
|
|
<button id="unsubscribeSymbolBtn" class="danger" disabled>取消品种</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Heartbeat Panel -->
|
|
<div class="panel">
|
|
<h2>心跳测试</h2>
|
|
<button id="pingBtn" disabled>发送 Ping</button>
|
|
</div>
|
|
|
|
<!-- Message Log Panel -->
|
|
<div class="panel">
|
|
<h2>消息日志</h2>
|
|
<div class="message-stats">
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="sentCount">0</div>
|
|
<div class="stat-label">已发送</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="receivedCount">0</div>
|
|
<div class="stat-label">已接收</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="tickCount">0</div>
|
|
<div class="stat-label">行情</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="orderCount">0</div>
|
|
<div class="stat-label">订单</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="tradeCount">0</div>
|
|
<div class="stat-label">成交</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="logCount">0</div>
|
|
<div class="stat-label">日志</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 15px;">
|
|
<button id="clearLogBtn">清空日志</button>
|
|
</div>
|
|
<div class="log-container" id="logContainer"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// WebSocket 客户端
|
|
let ws = null;
|
|
let sentCount = 0;
|
|
let receivedCount = 0;
|
|
let tickCount = 0;
|
|
let orderCount = 0;
|
|
let tradeCount = 0;
|
|
let logCount = 0;
|
|
|
|
// DOM 元素
|
|
const wsUrlInput = document.getElementById('wsUrl');
|
|
const tokenInput = document.getElementById('token');
|
|
const connectBtn = document.getElementById('connectBtn');
|
|
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
const statusSpan = document.getElementById('status');
|
|
const subscribeBtn = document.getElementById('subscribeBtn');
|
|
const unsubscribeBtn = document.getElementById('unsubscribeBtn');
|
|
const symbolInput = document.getElementById('symbolInput');
|
|
const subscribeSymbolBtn = document.getElementById('subscribeSymbolBtn');
|
|
const unsubscribeSymbolBtn = document.getElementById('unsubscribeSymbolBtn');
|
|
const pingBtn = document.getElementById('pingBtn');
|
|
const clearLogBtn = document.getElementById('clearLogBtn');
|
|
const logContainer = document.getElementById('logContainer');
|
|
|
|
// 连接 WebSocket
|
|
connectBtn.addEventListener('click', () => {
|
|
const url = wsUrlInput.value;
|
|
const token = tokenInput.value;
|
|
|
|
let wsUrl = url;
|
|
if (token) {
|
|
wsUrl += `?token=${token}`;
|
|
}
|
|
|
|
try {
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = () => {
|
|
updateStatus(true);
|
|
addLog('Connected', 'Connected to server', 'info');
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
const message = JSON.parse(event.data);
|
|
receivedCount++;
|
|
updateStats();
|
|
addLog('Received', JSON.stringify(message, null, 2), 'received');
|
|
|
|
// 统计消息类型
|
|
const msgType = message.type;
|
|
switch (msgType) {
|
|
case 'tick':
|
|
tickCount++;
|
|
break;
|
|
case 'order':
|
|
orderCount++;
|
|
break;
|
|
case 'trade':
|
|
tradeCount++;
|
|
break;
|
|
case 'log':
|
|
logCount++;
|
|
break;
|
|
}
|
|
updateStats();
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
addLog('Error', 'WebSocket error occurred', 'error');
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
updateStatus(false);
|
|
addLog('Disconnected', 'Connection closed', 'info');
|
|
};
|
|
} catch (error) {
|
|
addLog('Error', error.message, 'error');
|
|
}
|
|
});
|
|
|
|
// 断开连接
|
|
disconnectBtn.addEventListener('click', () => {
|
|
if (ws) {
|
|
ws.close();
|
|
ws = null;
|
|
}
|
|
});
|
|
|
|
// 订阅选中类型
|
|
subscribeBtn.addEventListener('click', () => {
|
|
const subscriptions = [];
|
|
document.querySelectorAll('.subscription-grid input[type="checkbox"]:checked').forEach(cb => {
|
|
subscriptions.push(cb.value);
|
|
});
|
|
|
|
if (subscriptions.length === 0) {
|
|
addLog('Warning', 'No subscriptions selected', 'info');
|
|
return;
|
|
}
|
|
|
|
sendMessage({
|
|
type: 'subscribe',
|
|
data: {
|
|
subscription: subscriptions
|
|
}
|
|
});
|
|
});
|
|
|
|
// 取消选中订阅
|
|
unsubscribeBtn.addEventListener('click', () => {
|
|
const subscriptions = [];
|
|
document.querySelectorAll('.subscription-grid input[type="checkbox"]:checked').forEach(cb => {
|
|
subscriptions.push(cb.value);
|
|
});
|
|
|
|
if (subscriptions.length === 0) {
|
|
addLog('Warning', 'No subscriptions selected', 'info');
|
|
return;
|
|
}
|
|
|
|
sendMessage({
|
|
type: 'unsubscribe',
|
|
data: {
|
|
subscription: subscriptions
|
|
}
|
|
});
|
|
});
|
|
|
|
// 订阅品种
|
|
subscribeSymbolBtn.addEventListener('click', () => {
|
|
const symbols = symbolInput.value.split(',').map(s => s.trim()).filter(s => s);
|
|
|
|
if (symbols.length === 0) {
|
|
addLog('Warning', 'No symbols specified', 'info');
|
|
return;
|
|
}
|
|
|
|
sendMessage({
|
|
type: 'subscribe_symbol',
|
|
data: {
|
|
symbol: symbols
|
|
}
|
|
});
|
|
});
|
|
|
|
// 取消品种订阅
|
|
unsubscribeSymbolBtn.addEventListener('click', () => {
|
|
const symbols = symbolInput.value.split(',').map(s => s.trim()).filter(s => s);
|
|
|
|
if (symbols.length === 0) {
|
|
addLog('Warning', 'No symbols specified', 'info');
|
|
return;
|
|
}
|
|
|
|
sendMessage({
|
|
type: 'unsubscribe_symbol',
|
|
data: {
|
|
symbol: symbols
|
|
}
|
|
});
|
|
});
|
|
|
|
// 发送 Ping
|
|
pingBtn.addEventListener('click', () => {
|
|
sendMessage({
|
|
type: 'ping',
|
|
data: {}
|
|
});
|
|
});
|
|
|
|
// 清空日志
|
|
clearLogBtn.addEventListener('click', () => {
|
|
logContainer.innerHTML = '';
|
|
sentCount = 0;
|
|
receivedCount = 0;
|
|
tickCount = 0;
|
|
orderCount = 0;
|
|
tradeCount = 0;
|
|
logCount = 0;
|
|
updateStats();
|
|
});
|
|
|
|
// 发送消息
|
|
function sendMessage(message) {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
addLog('Error', 'Not connected to server', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
ws.send(JSON.stringify(message));
|
|
sentCount++;
|
|
updateStats();
|
|
addLog('Sent', JSON.stringify(message, null, 2), 'sent');
|
|
} catch (error) {
|
|
addLog('Error', error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// 更新连接状态
|
|
function updateStatus(connected) {
|
|
if (connected) {
|
|
statusSpan.textContent = '已连接';
|
|
statusSpan.className = 'status connected';
|
|
connectBtn.disabled = true;
|
|
disconnectBtn.disabled = false;
|
|
subscribeBtn.disabled = false;
|
|
unsubscribeBtn.disabled = false;
|
|
subscribeSymbolBtn.disabled = false;
|
|
unsubscribeSymbolBtn.disabled = false;
|
|
pingBtn.disabled = false;
|
|
} else {
|
|
statusSpan.textContent = '未连接';
|
|
statusSpan.className = 'status disconnected';
|
|
connectBtn.disabled = false;
|
|
disconnectBtn.disabled = true;
|
|
subscribeBtn.disabled = true;
|
|
unsubscribeBtn.disabled = true;
|
|
subscribeSymbolBtn.disabled = true;
|
|
unsubscribeSymbolBtn.disabled = true;
|
|
pingBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
// 更新统计
|
|
function updateStats() {
|
|
document.getElementById('sentCount').textContent = sentCount;
|
|
document.getElementById('receivedCount').textContent = receivedCount;
|
|
document.getElementById('tickCount').textContent = tickCount;
|
|
document.getElementById('orderCount').textContent = orderCount;
|
|
document.getElementById('tradeCount').textContent = tradeCount;
|
|
document.getElementById('logCount').textContent = logCount;
|
|
}
|
|
|
|
// 添加日志
|
|
function addLog(direction, message, type) {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
const entry = document.createElement('div');
|
|
entry.className = `log-entry ${type}`;
|
|
entry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span><strong>${direction}:</strong> ${message}`;
|
|
logContainer.appendChild(entry);
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
}
|
|
|
|
// 初始化
|
|
updateStats();
|
|
</script>
|
|
</body>
|
|
</html>
|