You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
525 lines
17 KiB
525 lines
17 KiB
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>大宗商品实时监控大屏</title>
|
|
<!-- ECharts本地化,支持离线使用 -->
|
|
<script src="./echarts.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: 'Microsoft YaHei', 'SimHei', Arial, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
color: #fff;
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
}
|
|
.header {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
padding: 15px 30px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 2px solid rgba(0, 255, 255, 0.3);
|
|
}
|
|
.header h1 {
|
|
font-size: 28px;
|
|
color: #00d4ff;
|
|
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
|
}
|
|
.header .time {
|
|
font-size: 18px;
|
|
color: #ffd700;
|
|
}
|
|
.status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.status-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: #ff0000;
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
.status-dot.connected {
|
|
background: #00ff00;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
.container {
|
|
padding: 20px;
|
|
}
|
|
.price-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.price-card {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
transition: transform 0.3s, box-shadow 0.3s;
|
|
}
|
|
.price-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 10px 30px rgba(0, 212, 255, 0.3);
|
|
}
|
|
.price-card h3 {
|
|
font-size: 16px;
|
|
color: #aaa;
|
|
margin-bottom: 10px;
|
|
}
|
|
.price-card .price {
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
margin-bottom: 5px;
|
|
}
|
|
.price-card .change {
|
|
font-size: 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
.price-card .change.up {
|
|
color: #ff4444;
|
|
}
|
|
.price-card .change.down {
|
|
color: #00ff00;
|
|
}
|
|
.charts-row {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.chart-container {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
.chart-container h3 {
|
|
font-size: 18px;
|
|
color: #00d4ff;
|
|
margin-bottom: 15px;
|
|
border-left: 4px solid #00d4ff;
|
|
padding-left: 10px;
|
|
}
|
|
.chart {
|
|
height: 300px;
|
|
}
|
|
.ranking {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
.ranking h3 {
|
|
font-size: 18px;
|
|
color: #00d4ff;
|
|
margin-bottom: 15px;
|
|
border-left: 4px solid #00d4ff;
|
|
padding-left: 10px;
|
|
}
|
|
.ranking-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px;
|
|
margin-bottom: 8px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 8px;
|
|
transition: background 0.3s;
|
|
}
|
|
.ranking-item:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
.ranking-item .name {
|
|
font-size: 16px;
|
|
color: #fff;
|
|
}
|
|
.ranking-item .value {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
}
|
|
.ranking-item .value.up {
|
|
color: #ff4444;
|
|
}
|
|
.ranking-item .value.down {
|
|
color: #00ff00;
|
|
}
|
|
.sentiment-container {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
.sentiment-container h3 {
|
|
font-size: 18px;
|
|
color: #00d4ff;
|
|
margin-bottom: 15px;
|
|
border-left: 4px solid #00d4ff;
|
|
padding-left: 10px;
|
|
}
|
|
.sentiment-row {
|
|
display: flex;
|
|
gap: 20px;
|
|
}
|
|
.sentiment-item {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 15px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 10px;
|
|
}
|
|
.sentiment-item .label {
|
|
font-size: 14px;
|
|
color: #aaa;
|
|
margin-bottom: 5px;
|
|
}
|
|
.sentiment-item .percent {
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
}
|
|
.sentiment-item.bullish .percent {
|
|
color: #ff4444;
|
|
}
|
|
.sentiment-item.bearish .percent {
|
|
color: #00ff00;
|
|
}
|
|
.sentiment-item.neutral .percent {
|
|
color: #ffd700;
|
|
}
|
|
.alert-toast {
|
|
position: fixed;
|
|
top: 80px;
|
|
right: 20px;
|
|
background: rgba(255, 0, 0, 0.9);
|
|
color: #fff;
|
|
padding: 15px 25px;
|
|
border-radius: 10px;
|
|
display: none;
|
|
animation: slideIn 0.3s ease;
|
|
z-index: 1000;
|
|
}
|
|
.alert-toast.show {
|
|
display: block;
|
|
}
|
|
@keyframes slideIn {
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
.footer {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: #666;
|
|
font-size: 12px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>[大宗商品实时监控大屏]</h1>
|
|
<div class="status">
|
|
<span class="status-dot" id="statusDot"></span>
|
|
<span id="statusText">未连接</span>
|
|
</div>
|
|
<div class="time" id="currentTime">--</div>
|
|
</div>
|
|
|
|
<div class="alert-toast" id="alertToast">
|
|
<span id="alertText"></span>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="price-cards" id="priceCards">
|
|
<div class="price-card">
|
|
<h3>黄金 (XAU)</h3>
|
|
<div class="price" id="goldPrice">--</div>
|
|
<div class="change up" id="goldChange">--</div>
|
|
</div>
|
|
<div class="price-card">
|
|
<h3>白银 (XAG)</h3>
|
|
<div class="price" id="silverPrice">--</div>
|
|
<div class="change up" id="silverChange">--</div>
|
|
</div>
|
|
<div class="price-card">
|
|
<h3>原油 (WTI)</h3>
|
|
<div class="price" id="oilPrice">--</div>
|
|
<div class="change up" id="oilChange">--</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="charts-row">
|
|
<div class="chart-container">
|
|
<h3>实时价格走势图</h3>
|
|
<div class="chart" id="priceChart"></div>
|
|
</div>
|
|
<div class="ranking">
|
|
<h3>涨跌幅排行榜</h3>
|
|
<div id="rankingList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sentiment-container">
|
|
<h3>市场情绪分析</h3>
|
|
<div class="sentiment-row">
|
|
<div class="sentiment-item bullish">
|
|
<div class="label">利好情绪</div>
|
|
<div class="percent" id="bullishPercent">--%</div>
|
|
</div>
|
|
<div class="sentiment-item neutral">
|
|
<div class="label">中性情绪</div>
|
|
<div class="percent" id="neutralPercent">--%</div>
|
|
</div>
|
|
<div class="sentiment-item bearish">
|
|
<div class="label">利空情绪</div>
|
|
<div class="percent" id="bearishPercent">--%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
大宗商品爬虫系统 | 数据来源:金投网、东方财富、同花顺 | 实时更新中
|
|
</div>
|
|
|
|
<script>
|
|
let ws;
|
|
let priceChart;
|
|
let priceHistory = {
|
|
gold: [],
|
|
silver: [],
|
|
oil: []
|
|
};
|
|
|
|
const priceMap = {
|
|
'黄金': { element: 'goldPrice', changeElement: 'goldChange', history: 'gold' },
|
|
'白银': { element: 'silverPrice', changeElement: 'silverChange', history: 'silver' },
|
|
'原油': { element: 'oilPrice', changeElement: 'oilChange', history: 'oil' }
|
|
};
|
|
|
|
function init() {
|
|
initChart();
|
|
connectWebSocket();
|
|
updateTime();
|
|
setInterval(updateTime, 1000);
|
|
}
|
|
|
|
function initChart() {
|
|
priceChart = echarts.init(document.getElementById('priceChart'));
|
|
const option = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
legend: {
|
|
data: ['黄金', '白银', '原油'],
|
|
textStyle: { color: '#fff' },
|
|
top: 0
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
bottom: '3%',
|
|
top: '40px',
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
boundaryGap: false,
|
|
data: [],
|
|
axisLine: { lineStyle: { color: '#fff' } },
|
|
axisLabel: { color: '#aaa' }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLine: { lineStyle: { color: '#fff' } },
|
|
axisLabel: { color: '#aaa' },
|
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
|
},
|
|
series: [
|
|
{
|
|
name: '黄金',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: [],
|
|
lineStyle: { color: '#ffd700', width: 2 },
|
|
itemStyle: { color: '#ffd700' }
|
|
},
|
|
{
|
|
name: '白银',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: [],
|
|
lineStyle: { color: '#c0c0c0', width: 2 },
|
|
itemStyle: { color: '#c0c0c0' }
|
|
},
|
|
{
|
|
name: '原油',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: [],
|
|
lineStyle: { color: '#ff6b35', width: 2 },
|
|
itemStyle: { color: '#ff6b35' }
|
|
}
|
|
]
|
|
};
|
|
priceChart.setOption(option);
|
|
}
|
|
|
|
function connectWebSocket() {
|
|
const wsUrl = 'ws://localhost:8080';
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = function() {
|
|
document.getElementById('statusDot').classList.add('connected');
|
|
document.getElementById('statusText').textContent = '已连接';
|
|
console.log('WebSocket连接成功');
|
|
};
|
|
|
|
ws.onmessage = function(event) {
|
|
const message = JSON.parse(event.data);
|
|
handleMessage(message);
|
|
};
|
|
|
|
ws.onclose = function() {
|
|
document.getElementById('statusDot').classList.remove('connected');
|
|
document.getElementById('statusText').textContent = '连接断开';
|
|
console.log('WebSocket连接断开,5秒后重连...');
|
|
setTimeout(connectWebSocket, 5000);
|
|
};
|
|
|
|
ws.onerror = function(error) {
|
|
console.error('WebSocket错误:', error);
|
|
};
|
|
}
|
|
|
|
function handleMessage(message) {
|
|
switch(message.type) {
|
|
case 'PRICE_UPDATE':
|
|
updatePrices(message.data);
|
|
break;
|
|
case 'ALERT':
|
|
showAlert(message.data.variety, message.data.message);
|
|
break;
|
|
case 'HEARTBEAT':
|
|
console.log('心跳检测');
|
|
break;
|
|
}
|
|
}
|
|
|
|
function updatePrices(snapshots) {
|
|
snapshots.forEach(snapshot => {
|
|
const config = priceMap[snapshot.variety];
|
|
if (config) {
|
|
document.getElementById(config.element).textContent = snapshot.currentPrice;
|
|
|
|
const changeEl = document.getElementById(config.changeElement);
|
|
const changeRate = snapshot.changeRate;
|
|
changeEl.textContent = (changeRate >= 0 ? '+' : '') + changeRate.toFixed(2) + '%';
|
|
changeEl.className = 'change ' + (changeRate >= 0 ? 'up' : 'down');
|
|
|
|
priceHistory[config.history].push({
|
|
time: new Date(snapshot.timestamp).toLocaleTimeString(),
|
|
value: snapshot.currentPrice
|
|
});
|
|
|
|
if (priceHistory[config.history].length > 20) {
|
|
priceHistory[config.history].shift();
|
|
}
|
|
}
|
|
});
|
|
|
|
updateChart();
|
|
updateRanking(snapshots);
|
|
updateSentimentAnalysis(snapshots);
|
|
}
|
|
|
|
function updateSentimentAnalysis(snapshots) {
|
|
const upCount = snapshots.filter(s => s.changeRate > 0).length;
|
|
const downCount = snapshots.filter(s => s.changeRate < 0).length;
|
|
const neutralCount = snapshots.filter(s => s.changeRate === 0).length;
|
|
const total = snapshots.length;
|
|
|
|
const bullish = Math.round((upCount / total) * 35 + 35);
|
|
const bearish = Math.round((downCount / total) * 35 + 10);
|
|
const neutral = 100 - bullish - bearish;
|
|
|
|
updateSentiment(bullish, neutral, bearish);
|
|
}
|
|
|
|
function updateChart() {
|
|
const times = priceHistory.gold.map(p => p.time);
|
|
const goldData = priceHistory.gold.map(p => p.value);
|
|
const silverData = priceHistory.silver.map(p => p.value);
|
|
const oilData = priceHistory.oil.map(p => p.value);
|
|
|
|
priceChart.setOption({
|
|
xAxis: { data: times },
|
|
series: [
|
|
{ data: goldData },
|
|
{ data: silverData },
|
|
{ data: oilData }
|
|
]
|
|
});
|
|
}
|
|
|
|
function updateRanking(snapshots) {
|
|
const sorted = [...snapshots].sort((a, b) => b.changeRate - a.changeRate);
|
|
const rankingList = document.getElementById('rankingList');
|
|
rankingList.innerHTML = sorted.map((item, index) => `
|
|
<div class="ranking-item">
|
|
<span class="name">${index + 1}. ${item.variety}</span>
|
|
<span class="value ${item.changeRate >= 0 ? 'up' : 'down'}">
|
|
${item.changeRate >= 0 ? '+' : ''}${item.changeRate.toFixed(2)}%
|
|
</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function updateSentiment(bullish, neutral, bearish) {
|
|
document.getElementById('bullishPercent').textContent = bullish + '%';
|
|
document.getElementById('neutralPercent').textContent = neutral + '%';
|
|
document.getElementById('bearishPercent').textContent = bearish + '%';
|
|
}
|
|
|
|
function showAlert(variety, message) {
|
|
const toast = document.getElementById('alertToast');
|
|
document.getElementById('alertText').textContent = variety + ': ' + message;
|
|
toast.classList.add('show');
|
|
setTimeout(() => toast.classList.remove('show'), 5000);
|
|
}
|
|
|
|
function updateTime() {
|
|
const now = new Date();
|
|
document.getElementById('currentTime').textContent =
|
|
now.toLocaleDateString('zh-CN') + ' ' + now.toLocaleTimeString('zh-CN');
|
|
}
|
|
|
|
window.onload = init;
|
|
window.onresize = function() {
|
|
if (priceChart) {
|
|
priceChart.resize();
|
|
}
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|