update pay ui
This commit is contained in:
378
sse_html.html
Normal file
378
sse_html.html
Normal file
@@ -0,0 +1,378 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VDE 实时行情 - WebSocket 测试</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { color: #333; margin-bottom: 20px; }
|
||||
|
||||
.status-bar {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
}
|
||||
.status-indicator.connected { background: #4caf50; }
|
||||
.status-indicator.connecting { background: #ff9800; animation: pulse 1s infinite; }
|
||||
.status-indicator.disconnected { background: #f44336; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button.primary { background: #1976d2; color: white; }
|
||||
button.danger { background: #d32f2f; color: white; }
|
||||
button:hover { opacity: 0.9; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.quote-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
.quote-table th, .quote-table td {
|
||||
padding: 8px;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.quote-table th { text-align: left; color: #999; font-weight: normal; }
|
||||
.quote-table td:first-child { text-align: left; font-weight: 500; }
|
||||
.quote-table .name { color: #666; font-size: 12px; }
|
||||
|
||||
.price-up { color: #f44336; }
|
||||
.price-down { color: #4caf50; }
|
||||
.price-flat { color: #999; }
|
||||
|
||||
.update-flash { animation: flash 0.3s; }
|
||||
@keyframes flash { 0%, 100% { background: transparent; } 50% { background: #fff3e0; } }
|
||||
|
||||
#log {
|
||||
background: #263238;
|
||||
color: #aed581;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
}
|
||||
#log .error { color: #ef5350; }
|
||||
#log .info { color: #4fc3f7; }
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.stats span { font-weight: 500; color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>VDE 实时行情</h1>
|
||||
|
||||
<div class="status-bar">
|
||||
<div class="status-indicator" id="statusIndicator"></div>
|
||||
<span id="statusText">未连接</span>
|
||||
<div class="stats">
|
||||
<div>更新次数: <span id="updateCount">0</span></div>
|
||||
<div>最后更新: <span id="lastUpdate">-</span></div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input type="text" id="wsUrl" value="ws://localhost:8765" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; width: 200px;">
|
||||
<button class="primary" id="connectBtn" onclick="connect()">连接</button>
|
||||
<button class="danger" id="disconnectBtn" onclick="disconnect()" style="display:none">断开</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- 指数行情 -->
|
||||
<div class="card">
|
||||
<h2>📊 指数行情</h2>
|
||||
<table class="quote-table" id="indexTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>代码/名称</th>
|
||||
<th>最新</th>
|
||||
<th>涨跌</th>
|
||||
<th>涨跌幅</th>
|
||||
<th>成交额(亿)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 股票行情 -->
|
||||
<div class="card">
|
||||
<h2>📈 股票行情 (前20)</h2>
|
||||
<table class="quote-table" id="stockTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>代码/名称</th>
|
||||
<th>最新</th>
|
||||
<th>涨跌幅</th>
|
||||
<th>买一</th>
|
||||
<th>卖一</th>
|
||||
<th>成交额(万)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志 -->
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
let updateCount = 0;
|
||||
const indexData = {};
|
||||
const stockData = {};
|
||||
|
||||
function log(msg, type = '') {
|
||||
const logEl = document.getElementById('log');
|
||||
const time = new Date().toLocaleTimeString();
|
||||
logEl.innerHTML += `<div class="${type}">[${time}] ${msg}</div>`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
const indicator = document.getElementById('statusIndicator');
|
||||
const text = document.getElementById('statusText');
|
||||
const connectBtn = document.getElementById('connectBtn');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
|
||||
indicator.className = 'status-indicator ' + status;
|
||||
|
||||
switch(status) {
|
||||
case 'connected':
|
||||
text.textContent = '已连接';
|
||||
connectBtn.style.display = 'none';
|
||||
disconnectBtn.style.display = 'inline-block';
|
||||
break;
|
||||
case 'connecting':
|
||||
text.textContent = '连接中...';
|
||||
break;
|
||||
case 'disconnected':
|
||||
text.textContent = '未连接';
|
||||
connectBtn.style.display = 'inline-block';
|
||||
disconnectBtn.style.display = 'none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
const url = document.getElementById('wsUrl').value;
|
||||
log(`连接到 ${url}...`, 'info');
|
||||
setStatus('connecting');
|
||||
|
||||
try {
|
||||
ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = () => {
|
||||
log('WebSocket 连接成功', 'info');
|
||||
setStatus('connected');
|
||||
|
||||
// 订阅所有频道
|
||||
ws.send(JSON.stringify({
|
||||
action: 'subscribe',
|
||||
channels: ['index', 'stock', 'etf']
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
handleMessage(msg);
|
||||
} catch(e) {
|
||||
log('解析消息失败: ' + e, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
log('连接已断开', 'error');
|
||||
setStatus('disconnected');
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
log('连接错误', 'error');
|
||||
setStatus('disconnected');
|
||||
};
|
||||
} catch(e) {
|
||||
log('连接失败: ' + e, 'error');
|
||||
setStatus('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessage(msg) {
|
||||
if (msg.type === 'subscribed') {
|
||||
log(`已订阅: ${msg.channels.join(', ')}`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
updateCount++;
|
||||
document.getElementById('updateCount').textContent = updateCount;
|
||||
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
|
||||
|
||||
if (msg.type === 'index') {
|
||||
Object.assign(indexData, msg.data);
|
||||
renderIndexTable();
|
||||
} else if (msg.type === 'stock' || msg.type === 'etf') {
|
||||
Object.assign(stockData, msg.data);
|
||||
renderStockTable();
|
||||
}
|
||||
}
|
||||
|
||||
function formatPrice(price, prevClose) {
|
||||
if (!price || price === 0) return '-';
|
||||
const cls = price > prevClose ? 'price-up' : (price < prevClose ? 'price-down' : 'price-flat');
|
||||
return `<span class="${cls}">${price.toFixed(2)}</span>`;
|
||||
}
|
||||
|
||||
function formatChange(change, changePct) {
|
||||
if (change === undefined) return '-';
|
||||
const cls = change > 0 ? 'price-up' : (change < 0 ? 'price-down' : 'price-flat');
|
||||
const sign = change > 0 ? '+' : '';
|
||||
return `<span class="${cls}">${sign}${change.toFixed(2)}</span>`;
|
||||
}
|
||||
|
||||
function formatChangePct(changePct) {
|
||||
if (changePct === undefined) return '-';
|
||||
const cls = changePct > 0 ? 'price-up' : (changePct < 0 ? 'price-down' : 'price-flat');
|
||||
const sign = changePct > 0 ? '+' : '';
|
||||
return `<span class="${cls}">${sign}${changePct.toFixed(2)}%</span>`;
|
||||
}
|
||||
|
||||
function renderIndexTable() {
|
||||
const tbody = document.querySelector('#indexTable tbody');
|
||||
const importantIndexes = ['000001', '000002', '000003', '000016', '000300'];
|
||||
|
||||
let html = '';
|
||||
// 先显示重要指数
|
||||
for (const code of importantIndexes) {
|
||||
if (indexData[code]) {
|
||||
html += renderIndexRow(indexData[code]);
|
||||
}
|
||||
}
|
||||
// 再显示其他
|
||||
for (const [code, data] of Object.entries(indexData)) {
|
||||
if (!importantIndexes.includes(code)) {
|
||||
html += renderIndexRow(data);
|
||||
}
|
||||
if (Object.keys(indexData).length > 10) break;
|
||||
}
|
||||
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderIndexRow(data) {
|
||||
const change = data.last_price - data.prev_close;
|
||||
const changePct = data.prev_close > 0 ? (change / data.prev_close * 100) : 0;
|
||||
const amountYi = (data.amount / 100000000).toFixed(2);
|
||||
|
||||
return `
|
||||
<tr class="update-flash">
|
||||
<td>
|
||||
${data.security_id}
|
||||
<div class="name">${data.security_name}</div>
|
||||
</td>
|
||||
<td>${formatPrice(data.last_price, data.prev_close)}</td>
|
||||
<td>${formatChange(change)}</td>
|
||||
<td>${formatChangePct(changePct)}</td>
|
||||
<td>${amountYi}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderStockTable() {
|
||||
const tbody = document.querySelector('#stockTable tbody');
|
||||
const entries = Object.entries(stockData).slice(0, 20);
|
||||
|
||||
let html = '';
|
||||
for (const [code, data] of entries) {
|
||||
const changePct = data.prev_close > 0 ?
|
||||
((data.last_price - data.prev_close) / data.prev_close * 100) : 0;
|
||||
const amountWan = (data.amount / 10000).toFixed(0);
|
||||
|
||||
html += `
|
||||
<tr class="update-flash">
|
||||
<td>
|
||||
${data.security_id}
|
||||
<div class="name">${data.security_name}</div>
|
||||
</td>
|
||||
<td>${formatPrice(data.last_price, data.prev_close)}</td>
|
||||
<td>${formatChangePct(changePct)}</td>
|
||||
<td>${data.bid_prices?.[0]?.toFixed(2) || '-'}</td>
|
||||
<td>${data.ask_prices?.[0]?.toFixed(2) || '-'}</td>
|
||||
<td>${amountWan}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 页面加载后自动连接
|
||||
// window.onload = connect;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user