update pay ui
This commit is contained in:
@@ -402,59 +402,77 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const createConnection = useCallback((exchange: Exchange) => {
|
const createConnection = useCallback((exchange: Exchange) => {
|
||||||
|
// 检查是否是 HTTPS 页面尝试连接 ws://(Mixed Content)
|
||||||
|
const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
|
||||||
|
const wsUrl = WS_CONFIG[exchange];
|
||||||
|
const isInsecureWs = wsUrl.startsWith('ws://');
|
||||||
|
|
||||||
|
if (isHttps && isInsecureWs) {
|
||||||
|
logger.warn(
|
||||||
|
'FlexScreen',
|
||||||
|
`${exchange} WebSocket 连接被跳过:HTTPS 页面无法连接不安全的 ws:// 端点`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wsRefs.current[exchange]) {
|
if (wsRefs.current[exchange]) {
|
||||||
wsRefs.current[exchange]!.close();
|
wsRefs.current[exchange]!.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
const ws = new WebSocket(WS_CONFIG[exchange]);
|
try {
|
||||||
wsRefs.current[exchange] = ws;
|
const ws = new WebSocket(wsUrl);
|
||||||
|
wsRefs.current[exchange] = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
logger.info('FlexScreen', `${exchange} WebSocket 已连接`);
|
logger.info('FlexScreen', `${exchange} WebSocket 已连接`);
|
||||||
setConnected(prev => ({ ...prev, [exchange]: true }));
|
setConnected(prev => ({ ...prev, [exchange]: true }));
|
||||||
|
|
||||||
if (exchange === 'SSE') {
|
if (exchange === 'SSE') {
|
||||||
const codes = Array.from(subscribedCodes.current.SSE);
|
const codes = Array.from(subscribedCodes.current.SSE);
|
||||||
if (codes.length > 0) {
|
if (codes.length > 0) {
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
action: 'subscribe',
|
action: 'subscribe',
|
||||||
channels: ['stock', 'index'],
|
channels: ['stock', 'index'],
|
||||||
codes,
|
codes,
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startHeartbeat(exchange);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event: MessageEvent) => {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(event.data);
|
|
||||||
handleMessage(exchange, msg);
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('FlexScreen', `${exchange} 消息解析失败`, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error: Event) => {
|
|
||||||
logger.error('FlexScreen', `${exchange} WebSocket 错误`, error);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
logger.info('FlexScreen', `${exchange} WebSocket 断开`);
|
|
||||||
setConnected(prev => ({ ...prev, [exchange]: false }));
|
|
||||||
stopHeartbeat(exchange);
|
|
||||||
|
|
||||||
// 自动重连
|
|
||||||
if (!reconnectRefs.current[exchange] && subscribedCodes.current[exchange].size > 0) {
|
|
||||||
reconnectRefs.current[exchange] = setTimeout(() => {
|
|
||||||
reconnectRefs.current[exchange] = null;
|
|
||||||
if (subscribedCodes.current[exchange].size > 0) {
|
|
||||||
createConnection(exchange);
|
|
||||||
}
|
}
|
||||||
}, RECONNECT_INTERVAL);
|
}
|
||||||
}
|
|
||||||
};
|
startHeartbeat(exchange);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
handleMessage(exchange, msg);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('FlexScreen', `${exchange} 消息解析失败`, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error: Event) => {
|
||||||
|
logger.error('FlexScreen', `${exchange} WebSocket 错误`, error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
logger.info('FlexScreen', `${exchange} WebSocket 断开`);
|
||||||
|
setConnected(prev => ({ ...prev, [exchange]: false }));
|
||||||
|
stopHeartbeat(exchange);
|
||||||
|
|
||||||
|
// 自动重连(仅在非 HTTPS + ws:// 场景下)
|
||||||
|
if (!reconnectRefs.current[exchange] && subscribedCodes.current[exchange].size > 0) {
|
||||||
|
reconnectRefs.current[exchange] = setTimeout(() => {
|
||||||
|
reconnectRefs.current[exchange] = null;
|
||||||
|
if (subscribedCodes.current[exchange].size > 0) {
|
||||||
|
createConnection(exchange);
|
||||||
|
}
|
||||||
|
}, RECONNECT_INTERVAL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('FlexScreen', `${exchange} WebSocket 连接失败`, e);
|
||||||
|
setConnected(prev => ({ ...prev, [exchange]: false }));
|
||||||
|
}
|
||||||
}, [startHeartbeat, stopHeartbeat, handleMessage]);
|
}, [startHeartbeat, stopHeartbeat, handleMessage]);
|
||||||
|
|
||||||
const subscribe = useCallback((code: string) => {
|
const subscribe = useCallback((code: string) => {
|
||||||
|
|||||||
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>
|
||||||
289
szse_html.html
Normal file
289
szse_html.html
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>深交所行情 WebSocket 测试</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Microsoft YaHei', sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }
|
||||||
|
h1 { text-align: center; margin-bottom: 20px; color: #00d4ff; }
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; }
|
||||||
|
.controls { background: #16213e; padding: 15px; border-radius: 8px; margin-bottom: 20px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||||
|
input { padding: 8px 12px; border: 1px solid #0f3460; border-radius: 4px; background: #1a1a2e; color: #fff; width: 300px; }
|
||||||
|
button { padding: 8px 20px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
|
||||||
|
.btn-connect { background: #00d4ff; color: #000; }
|
||||||
|
.btn-disconnect { background: #e94560; color: #fff; }
|
||||||
|
.btn-ping { background: #ffc107; color: #000; }
|
||||||
|
.btn-clear { background: #6c757d; color: #fff; }
|
||||||
|
.status { padding: 5px 15px; border-radius: 20px; font-size: 14px; }
|
||||||
|
.status.connected { background: #28a745; }
|
||||||
|
.status.disconnected { background: #dc3545; }
|
||||||
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 20px; }
|
||||||
|
.stat-card { background: #16213e; padding: 15px; border-radius: 8px; text-align: center; }
|
||||||
|
.stat-card .value { font-size: 24px; font-weight: bold; color: #00d4ff; }
|
||||||
|
.stat-card .label { font-size: 12px; color: #888; margin-top: 5px; }
|
||||||
|
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||||
|
@media (max-width: 900px) { .panels { grid-template-columns: 1fr; } }
|
||||||
|
.panel { background: #16213e; border-radius: 8px; overflow: hidden; }
|
||||||
|
.panel-header { background: #0f3460; padding: 10px 15px; font-weight: bold; display: flex; justify-content: space-between; }
|
||||||
|
.panel-body { height: 400px; overflow-y: auto; padding: 10px; font-family: 'Consolas', monospace; font-size: 12px; }
|
||||||
|
.msg { padding: 5px; border-bottom: 1px solid #0f3460; word-break: break-all; }
|
||||||
|
.msg.snapshot { color: #ffc107; }
|
||||||
|
.msg.stock { color: #28a745; }
|
||||||
|
.msg.index { color: #17a2b8; }
|
||||||
|
.msg.bond { color: #6f42c1; }
|
||||||
|
.msg.hk_stock { color: #fd7e14; }
|
||||||
|
.msg.afterhours_block, .msg.afterhours_trading { color: #e83e8c; }
|
||||||
|
.msg.volume_stats { color: #20c997; }
|
||||||
|
.msg.fund_nav { color: #6610f2; }
|
||||||
|
.msg.pong { color: #adb5bd; }
|
||||||
|
.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 #0f3460; }
|
||||||
|
.quote-table th { background: #0f3460; text-align: center; }
|
||||||
|
.quote-table .code { text-align: left; font-weight: bold; }
|
||||||
|
.quote-table .up { color: #f5222d; }
|
||||||
|
.quote-table .down { color: #52c41a; }
|
||||||
|
.quote-table .flat { color: #888; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>深交所行情 WebSocket 测试</h1>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<input type="text" id="wsUrl" value="ws://222.128.1.157:8765" placeholder="WebSocket URL">
|
||||||
|
<button class="btn-connect" onclick="connect()">连接</button>
|
||||||
|
<button class="btn-disconnect" onclick="disconnect()">断开</button>
|
||||||
|
<button class="btn-ping" onclick="sendPing()">Ping</button>
|
||||||
|
<button class="btn-clear" onclick="clearLogs()">清空日志</button>
|
||||||
|
<span id="status" class="status disconnected">未连接</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="msgCount">0</div>
|
||||||
|
<div class="label">消息总数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="stockCount">0</div>
|
||||||
|
<div class="label">股票 (300111)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="indexCount">0</div>
|
||||||
|
<div class="label">指数 (309011)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="bondCount">0</div>
|
||||||
|
<div class="label">债券 (300211)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="hkCount">0</div>
|
||||||
|
<div class="label">港股 (306311)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="value" id="otherCount">0</div>
|
||||||
|
<div class="label">其他类型</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panels">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>实时行情</span>
|
||||||
|
<span id="quoteTime">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body" style="padding: 0;">
|
||||||
|
<table class="quote-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>代码</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>最新价</th>
|
||||||
|
<th>涨跌幅</th>
|
||||||
|
<th>成交量</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="quoteTable"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>消息日志</span>
|
||||||
|
<span id="logCount">0 条</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body" id="logs"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
let msgCount = 0;
|
||||||
|
let counts = { stock: 0, index: 0, bond: 0, hk_stock: 0, other: 0 };
|
||||||
|
let quotes = {};
|
||||||
|
const maxLogs = 200;
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
const url = document.getElementById('wsUrl').value;
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
addLog('已经连接', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
setStatus(true);
|
||||||
|
addLog(`已连接到 ${url}`, 'info');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
msgCount++;
|
||||||
|
document.getElementById('msgCount').textContent = msgCount;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
handleMessage(msg);
|
||||||
|
} catch (err) {
|
||||||
|
addLog(`解析错误: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
setStatus(false);
|
||||||
|
addLog('连接已断开', 'warning');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
addLog(`连接错误`, 'error');
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
addLog(`连接失败: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendPing() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ping' }));
|
||||||
|
addLog('发送 Ping', 'info');
|
||||||
|
} else {
|
||||||
|
addLog('未连接', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessage(msg) {
|
||||||
|
if (msg.type === 'snapshot') {
|
||||||
|
addLog(`收到快照: 股票${msg.data.stocks?.length || 0}只, 指数${msg.data.indexes?.length || 0}个, 债券${msg.data.bonds?.length || 0}只`, 'snapshot');
|
||||||
|
// 初始化行情表
|
||||||
|
msg.data.stocks?.slice(-20).forEach(s => updateQuote('stock', s));
|
||||||
|
msg.data.indexes?.forEach(s => updateQuote('index', s));
|
||||||
|
} else if (msg.type === 'realtime') {
|
||||||
|
const cat = msg.category;
|
||||||
|
if (cat === 'stock') { counts.stock++; updateQuote('stock', msg.data); }
|
||||||
|
else if (cat === 'index') { counts.index++; updateQuote('index', msg.data); }
|
||||||
|
else if (cat === 'bond') { counts.bond++; updateQuote('bond', msg.data); }
|
||||||
|
else if (cat === 'hk_stock') { counts.hk_stock++; updateQuote('hk_stock', msg.data); }
|
||||||
|
else { counts.other++; }
|
||||||
|
|
||||||
|
document.getElementById('stockCount').textContent = counts.stock;
|
||||||
|
document.getElementById('indexCount').textContent = counts.index;
|
||||||
|
document.getElementById('bondCount').textContent = counts.bond;
|
||||||
|
document.getElementById('hkCount').textContent = counts.hk_stock;
|
||||||
|
document.getElementById('otherCount').textContent = counts.other;
|
||||||
|
|
||||||
|
// 每50条记录一次日志
|
||||||
|
if (msgCount % 50 === 0) {
|
||||||
|
addLog(`${cat}: ${msg.data.security_id} = ${msg.data.last_px || msg.data.current_index || '--'}`, cat);
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'pong') {
|
||||||
|
addLog('收到 Pong', 'pong');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('quoteTime').textContent = new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQuote(type, data) {
|
||||||
|
const id = data.security_id;
|
||||||
|
quotes[id] = { type, ...data };
|
||||||
|
renderQuotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQuotes() {
|
||||||
|
const tbody = document.getElementById('quoteTable');
|
||||||
|
const sorted = Object.values(quotes).sort((a, b) => {
|
||||||
|
const order = { index: 0, stock: 1, hk_stock: 2, bond: 3 };
|
||||||
|
return (order[a.type] || 9) - (order[b.type] || 9);
|
||||||
|
}).slice(0, 30);
|
||||||
|
|
||||||
|
tbody.innerHTML = sorted.map(q => {
|
||||||
|
const price = q.last_px || q.current_index || 0;
|
||||||
|
const prev = q.prev_close || 0;
|
||||||
|
const change = prev ? ((price - prev) / prev * 100).toFixed(2) : '0.00';
|
||||||
|
const cls = change > 0 ? 'up' : change < 0 ? 'down' : 'flat';
|
||||||
|
const typeMap = { stock: '股票', index: '指数', bond: '债券', hk_stock: '港股' };
|
||||||
|
return `<tr>
|
||||||
|
<td class="code">${q.security_id}</td>
|
||||||
|
<td>${typeMap[q.type] || q.type}</td>
|
||||||
|
<td class="${cls}">${price.toFixed(q.type === 'index' ? 2 : 2)}</td>
|
||||||
|
<td class="${cls}">${change}%</td>
|
||||||
|
<td>${formatVolume(q.volume)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatVolume(v) {
|
||||||
|
if (!v) return '--';
|
||||||
|
if (v >= 100000000) return (v / 100000000).toFixed(2) + '亿';
|
||||||
|
if (v >= 10000) return (v / 10000).toFixed(2) + '万';
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLog(text, type = 'info') {
|
||||||
|
const logs = document.getElementById('logs');
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `msg ${type}`;
|
||||||
|
div.textContent = `[${time}] ${text}`;
|
||||||
|
logs.insertBefore(div, logs.firstChild);
|
||||||
|
|
||||||
|
// 限制日志数量
|
||||||
|
while (logs.children.length > maxLogs) {
|
||||||
|
logs.removeChild(logs.lastChild);
|
||||||
|
}
|
||||||
|
document.getElementById('logCount').textContent = `${logs.children.length} 条`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogs() {
|
||||||
|
document.getElementById('logs').innerHTML = '';
|
||||||
|
document.getElementById('logCount').textContent = '0 条';
|
||||||
|
msgCount = 0;
|
||||||
|
counts = { stock: 0, index: 0, bond: 0, hk_stock: 0, other: 0 };
|
||||||
|
document.getElementById('msgCount').textContent = '0';
|
||||||
|
document.getElementById('stockCount').textContent = '0';
|
||||||
|
document.getElementById('indexCount').textContent = '0';
|
||||||
|
document.getElementById('bondCount').textContent = '0';
|
||||||
|
document.getElementById('hkCount').textContent = '0';
|
||||||
|
document.getElementById('otherCount').textContent = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(connected) {
|
||||||
|
const el = document.getElementById('status');
|
||||||
|
el.textContent = connected ? '已连接' : '未连接';
|
||||||
|
el.className = `status ${connected ? 'connected' : 'disconnected'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面关闭时断开连接
|
||||||
|
window.onbeforeunload = () => { if (ws) ws.close(); };
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -112,6 +112,42 @@ server {
|
|||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 实时行情 WebSocket 代理(灵活屏功能)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# 上交所实时行情 WebSocket
|
||||||
|
location /ws/sse {
|
||||||
|
proxy_pass http://49.232.185.254:8765;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 7d;
|
||||||
|
proxy_send_timeout 7d;
|
||||||
|
proxy_read_timeout 7d;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 深交所实时行情 WebSocket
|
||||||
|
location /ws/szse {
|
||||||
|
proxy_pass http://222.128.1.157:8765;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 7d;
|
||||||
|
proxy_send_timeout 7d;
|
||||||
|
proxy_read_timeout 7d;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
location /mcp/ {
|
location /mcp/ {
|
||||||
proxy_pass http://127.0.0.1:8900/;
|
proxy_pass http://127.0.0.1:8900/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -142,7 +178,6 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# 概念板块API代理
|
# 概念板块API代理
|
||||||
location /concept-api/ {
|
location /concept-api/ {
|
||||||
proxy_pass http://222.128.1.157:16801/;
|
proxy_pass http://222.128.1.157:16801/;
|
||||||
@@ -158,6 +193,7 @@ server {
|
|||||||
proxy_send_timeout 60s;
|
proxy_send_timeout 60s;
|
||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 60s;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Elasticsearch API代理(价值论坛)
|
# Elasticsearch API代理(价值论坛)
|
||||||
location /es-api/ {
|
location /es-api/ {
|
||||||
proxy_pass http://222.128.1.157:19200/;
|
proxy_pass http://222.128.1.157:19200/;
|
||||||
@@ -223,36 +259,7 @@ server {
|
|||||||
proxy_send_timeout 86400s;
|
proxy_send_timeout 86400s;
|
||||||
proxy_read_timeout 86400s;
|
proxy_read_timeout 86400s;
|
||||||
}
|
}
|
||||||
# AI Chat 应用 (Next.js) - MCP 集成
|
|
||||||
# AI Chat 静态资源(图片、CSS、JS)
|
|
||||||
location ~ ^/ai-chat/(images|_next/static|_next/image|favicon.ico) {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# 缓存设置
|
|
||||||
expires 30d;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# AI Chat 主应用
|
|
||||||
location /ai-chat {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Cookie $http_cookie;
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_cache off;
|
|
||||||
}
|
|
||||||
# iframe 内部资源代理(Bytedesk 聊天窗口的 CSS/JS)
|
# iframe 内部资源代理(Bytedesk 聊天窗口的 CSS/JS)
|
||||||
location /chat/ {
|
location /chat/ {
|
||||||
proxy_pass http://43.143.189.195/chat/;
|
proxy_pass http://43.143.189.195/chat/;
|
||||||
@@ -326,6 +333,22 @@ server {
|
|||||||
add_header Cache-Control "public, max-age=86400";
|
add_header Cache-Control "public, max-age=86400";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Bytedesk 文件访问代理(仅 2025 年文件)
|
||||||
|
location ^~ /file/2025/ {
|
||||||
|
proxy_pass http://43.143.189.195/file/2025/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# 缓存配置
|
||||||
|
proxy_cache_valid 200 1d;
|
||||||
|
expires 1d;
|
||||||
|
add_header Cache-Control "public, max-age=86400";
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Visitor API 代理(Bytedesk 初始化接口)
|
# Visitor API 代理(Bytedesk 初始化接口)
|
||||||
location /visitor/ {
|
location /visitor/ {
|
||||||
|
|||||||
Reference in New Issue
Block a user