231 lines
7.3 KiB
Plaintext
231 lines
7.3 KiB
Plaintext
<!--下面全是嵌入式卡片的代码,复制后找个位置直接粘贴就行,个人建议body的最下面-->
|
||
<!--记得更改serverUrl(地址,端口,后面建议别动)-->
|
||
|
||
|
||
<div id="minecraft-card-container"></div>
|
||
|
||
<script>
|
||
// 感谢deepseek大爹!!!
|
||
// ===== 配置 =====
|
||
const config = {
|
||
serverUrl: "http://localhost:60048/api/widget-data",
|
||
updateInterval: 5000,
|
||
cardWidth: "300px"
|
||
};
|
||
|
||
(function initCard() {
|
||
if(localStorage.getItem('mcCardClosed')) return;
|
||
|
||
const card = createCard();
|
||
document.getElementById('minecraft-card-container').appendChild(card);
|
||
setupDragAndClose(card);
|
||
|
||
fetchData(card).then(() => {
|
||
setInterval(() => fetchData(card), config.updateInterval);
|
||
});
|
||
|
||
setTimeout(() => card.style.display = 'block', 100);
|
||
})();
|
||
|
||
function createCard() {
|
||
const card = document.createElement('div');
|
||
card.id = 'mc-status-card';
|
||
card.style.cssText = `
|
||
position: fixed; width: ${config.cardWidth}; z-index: 99999;
|
||
right: 20px; bottom: 20px; border-radius: 12px; background: white;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.15); font-family: 'Segoe UI', sans-serif;
|
||
overflow: hidden; transition: all 0.2s; display: none;
|
||
`;
|
||
|
||
card.innerHTML = `
|
||
<div class="card-header" style="cursor:move;background:linear-gradient(135deg,#6a11cb 0%,#2575fc 100%);
|
||
color:white;padding:12px 15px;position:relative;">
|
||
<span>服务器玩家状态</span>
|
||
<span class="close-btn" style="position:absolute;right:10px;top:10px;color:white;
|
||
opacity:0.8;cursor:pointer;transition:opacity 0.2s;">×</span>
|
||
</div>
|
||
<div class="card-body" style="padding:0;">
|
||
<div class="loading" style="padding:20px;text-align:center;">
|
||
<div style="width:20px;height:20px;margin:0 auto 10px;border:3px solid rgba(0,0,0,0.1);
|
||
border-radius:50%;border-top-color:#2575fc;animation:spin 1s linear infinite;"></div>
|
||
<p>连接服务器中...</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
return card;
|
||
}
|
||
|
||
async function fetchData(card) {
|
||
const body = card.querySelector('.card-body');
|
||
|
||
try {
|
||
// 方案1:尝试直接请求(如果同源)
|
||
let data = await tryDirectFetch();
|
||
|
||
// 方案2:如果失败则使用服务器端代理
|
||
if(!data) data = await tryServerProxy();
|
||
|
||
if(data) {
|
||
renderCard(body, data);
|
||
} else {
|
||
throw new Error('所有数据获取方式均失败');
|
||
}
|
||
} catch(error) {
|
||
body.innerHTML = `
|
||
<div style="padding:20px;text-align:center;">
|
||
<p style="color:#dc3545;">⚠️ 数据加载失败</p>
|
||
<small style="color:#6c757d;">${error.message}</small>
|
||
<button onclick="location.reload()" style="margin-top:10px;padding:5px 10px;
|
||
background:#f8f9fa;border:1px solid #ddd;border-radius:4px;cursor:pointer;">
|
||
重试
|
||
</button>
|
||
</div>
|
||
`;
|
||
console.error('数据获取失败:', error);
|
||
}
|
||
}
|
||
|
||
async function tryDirectFetch() {
|
||
try {
|
||
const response = await fetch(config.serverUrl, {
|
||
headers: { 'Accept': 'application/json' }
|
||
});
|
||
return await fetchWidgetData();
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
async function fetchWidgetData() {
|
||
try {
|
||
const response = await fetch('http://localhost:60048/api/widget-data');
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('获取到的数据:', data);
|
||
return data;
|
||
} catch (error) {
|
||
console.error('获取widget数据时出错:', error);
|
||
return {
|
||
onlineCount: 0,
|
||
whitelistPlayers: [],
|
||
topPlayers: [],
|
||
timestamp: Date.now()
|
||
};
|
||
}
|
||
}
|
||
|
||
|
||
|
||
async function tryServerProxy() {
|
||
try {
|
||
// 备用的,暂时不用,deepseek建议留着
|
||
const proxyUrl = 'http:// ' +
|
||
encodeURIComponent(config.serverUrl);
|
||
|
||
const response = await fetch(proxyUrl);
|
||
return await response.json();
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function renderCard(container, data) {
|
||
container.innerHTML = `
|
||
<div style="display:flex;align-items:center;padding:10px 15px;border-bottom:1px solid rgba(0,0,0,0.05);">
|
||
<span style="font-size:1.2rem;margin-right:12px;color:#2575fc;">👥</span>
|
||
<span>在线玩家</span>
|
||
<span style="margin-left:auto;background:#0d6efd;color:white;
|
||
padding:0.25em 0.6em;border-radius:50rem;font-size:0.75em;">
|
||
${data.onlineCount}
|
||
</span>
|
||
</div>
|
||
|
||
${data.topPlayers?.length ? `
|
||
<div style="padding:8px 15px;background:#f8f9fa;">
|
||
<small><span style="color:#ffc107">🏆</span> 时长前三</small>
|
||
</div>
|
||
${data.topPlayers.map(p => renderPlayer(p)).join('')}
|
||
` : ''}
|
||
|
||
<div style="padding:8px 15px;background:#f8f9fa;">
|
||
<small><span style="color:#2575fc">ⓘ</span> 在线玩家</small>
|
||
</div>
|
||
|
||
<div style="max-height:150px;overflow-y:auto;">
|
||
${data.whitelistPlayers?.length ?
|
||
data.whitelistPlayers.map(p => renderPlayer(p)).join('') :
|
||
'<div style="padding:15px;text-align:center;color:#6c757d;font-style:italic">无在线玩家</div>'
|
||
}
|
||
</div>
|
||
|
||
<div style="padding:8px;text-align:center;color:#6c757d;font-size:0.8em;">
|
||
更新: ${formatTime(data.timestamp)}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderPlayer(player) {
|
||
return `
|
||
<div style="display:flex;align-items:center;padding:8px 15px;border-bottom:1px solid rgba(0,0,0,0.05);">
|
||
<div style="width:24px;height:24px;border-radius:50%;background:#e9ecef;margin-right:10px;
|
||
display:flex;align-items:center;justify-content:center;font-size:12px;color:#495057;">
|
||
${player.name.charAt(0).toUpperCase()}
|
||
</div>
|
||
<span style="flex-grow:1;">${player.name}</span>
|
||
<span style="font-size:0.9rem;color:#6c757d;">${player.time}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function setupDragAndClose(card) {
|
||
const header = card.querySelector('.card-header');
|
||
let isDragging = false, offsetX, offsetY;
|
||
|
||
header.addEventListener('mousedown', (e) => {
|
||
if(e.target.classList.contains('close-btn')) return;
|
||
isDragging = true;
|
||
offsetX = e.clientX - card.getBoundingClientRect().left;
|
||
offsetY = e.clientY - card.getBoundingClientRect().top;
|
||
card.style.cursor = 'grabbing';
|
||
});
|
||
|
||
document.addEventListener('mousemove', (e) => {
|
||
if(!isDragging) return;
|
||
card.style.left = (e.clientX - offsetX) + 'px';
|
||
card.style.top = (e.clientY - offsetY) + 'px';
|
||
card.style.right = 'auto';
|
||
card.style.bottom = 'auto';
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
isDragging = false;
|
||
card.style.cursor = 'grab';
|
||
});
|
||
|
||
card.querySelector('.close-btn').addEventListener('click', () => {
|
||
card.style.display = 'none';
|
||
localStorage.setItem('mcCardClosed', 'true');
|
||
});
|
||
}
|
||
|
||
function formatTime(timestamp) {
|
||
const now = Date.now();
|
||
const diff = (now - timestamp) / 1000;
|
||
if(diff < 60) return '刚刚';
|
||
if(diff < 3600) return `${Math.floor(diff/60)}分钟前`;
|
||
return `${Math.floor(diff/3600)}小时前`;
|
||
}
|
||
|
||
document.head.insertAdjacentHTML('beforeend', `
|
||
<style>
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
#mc-status-card:hover { transform: translateY(-2px); box-shadow: 0 6px 25px rgba(0,0,0,0.2); }
|
||
</style>
|
||
`);
|
||
</script> |