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

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>