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

<!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>