UNPKG

koishi-plugin-tmp-bot

Version:

欧洲卡车模拟2 TMP查询插件,不会部署的可以直接使用此机器人->QQ:3523283907

299 lines (265 loc) 7.61 kB
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Server List</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; background-color: #0b1120; } #container { width: 380px; background: linear-gradient(180deg, #0f1a2e 0%, #0b1120 100%); padding: 0 0 6px 0; } /* Header */ .header { height: 42px; background-color: rgba(0, 0, 0, 0.3); display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4); } .header .title { color: #8bafff; font-size: 14px; font-weight: 600; letter-spacing: 1px; } /* Server list */ .server-list { padding: 6px 10px 0 10px; display: flex; flex-direction: column; gap: 5px; } /* Server card */ .server-card { background: rgba(22, 33, 55, 0.8); border-radius: 6px; border: 1px solid rgba(139, 175, 255, 0.08); overflow: hidden; } /* Card info */ .server-info { padding: 7px 12px 5px 12px; } .server-row { display: flex; align-items: center; justify-content: space-between; } .server-left { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; } .status-indicator { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; } .status-indicator.online { background-color: #34d058; box-shadow: 0 0 5px rgba(52, 208, 88, 0.5); } .status-indicator.offline { background-color: #484f58; } .server-name { color: #e6edf3; font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; } .player-count { color: #6e7a8a; font-size: 11px; white-space: nowrap; flex-shrink: 0; margin-left: 6px; } .player-count .num { color: #c9d1d9; font-weight: 700; font-size: 12px; } /* Features row */ .features { display: flex; align-items: center; gap: 8px; margin-top: 3px; } .feature { font-size: 10px; color: #5a6572; white-space: nowrap; } /* Queue badge */ .queue-badge { color: #f0883e; font-size: 10px; margin-left: 6px; } /* Chart area */ .chart-area { position: relative; height: 38px; } .chart-area svg { display: block; width: 100%; height: 100%; } </style> </head> <body> <div id="container"> <div class="header"> <div class="title">TruckersMP 服务器状态</div> </div> <div class="server-list" id="server-list"></div> </div> <script> /** * 构建 SVG 面积折线图 * maxPlayer 的 50% 作为余量,上限不超过 maxPlayer */ function buildChartSVG(data, width, height, maxPlayer) { const padTop = 2; const padBottom = 0; const chartW = width; const chartH = height - padTop - padBottom; const dataMax = Math.max(...data, 1); const max = Math.min(maxPlayer, maxPlayer * 0.12 + dataMax); const step = chartW / (data.length - 1); const points = data.map((v, i) => { const x = i * step; const y = padTop + chartH - (v / max) * chartH; return [x, y]; }); const lineD = points.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); const areaD = lineD + ` L${points[points.length - 1][0]},${padTop + chartH}` + ` L${points[0][0]},${padTop + chartH} Z`; const gid = 'g' + Math.random().toString(36).slice(2, 8); return ` <svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"> <defs> <linearGradient id="${gid}" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="rgba(79,139,255,0.4)"/> <stop offset="100%" stop-color="rgba(79,139,255,0.02)"/> </linearGradient> </defs> <path d="${areaD}" fill="url(#${gid})"/> <path d="${lineD}" fill="none" stroke="#4f8bff" stroke-width="1.5" stroke-linejoin="round"/> </svg>`; } /** * 将 playerHistory 标准化为 288 个点(24h,每5分钟一个点) * 缺失的时间段填充为 0 * @param {Array<{updateTime: string, playerCount: number}>} playerHistory */ function normalizeHistory(playerHistory) { const SLOT_COUNT = 288; const SLOT_MS = 300000; if (!playerHistory || playerHistory.length === 0) return []; // API 返回的是北京时间(UTC+8),用 Date.UTC 生成时间戳再减去8小时偏移 function parseTime(str) { const [datePart, timePart] = str.trim().replace('T', ' ').split(' '); const [y, m, d] = datePart.split('-').map(Number); const parts = timePart.split(':').map(Number); const h = parts[0] || 0, min = parts[1] || 0; return Date.UTC(y, m - 1, d, h, min, 0) - 8 * 3600000; } const now = Date.now(); const currentSlot = Math.floor(now / SLOT_MS) * SLOT_MS; const dataMap = {}; for (const item of playerHistory) { const ts = parseTime(item.updateTime); if (isNaN(ts)) continue; const slot = Math.floor(ts / SLOT_MS) * SLOT_MS; dataMap[slot] = item.playerCount; } const result = []; for (let i = 0; i < SLOT_COUNT; i++) { const slotTime = currentSlot - (SLOT_COUNT - 1 - i) * SLOT_MS; result.push(dataMap[slotTime] !== undefined ? dataMap[slotTime] : 0); } return result; } /** * 渲染一个服务器卡片 */ function createServerCard(server) { const card = document.createElement('div'); card.className = 'server-card'; // 只展示存在的特性 let featuresHTML = ''; if (server.collisionsEnable === 1) { featuresHTML += '<span class="feature">💥 碰撞</span>'; } if (server.afkEnable === 0) { featuresHTML += '<span class="feature">💤 挂机</span>'; } if (server.policeCarEnable === 1) { featuresHTML += '<span class="feature">🚓 警车</span>'; } if (server.speedLimiterEnable === 1) { featuresHTML += '<span class="feature">🐢 限速</span>'; } // 队列 let queueText = ''; if (server.queueCount > 0) { queueText = `<span class="queue-badge">队列 ${server.queueCount}</span>`; } card.innerHTML = ` <div class="server-info"> <div class="server-row"> <div class="server-left"> <span class="status-indicator ${server.isOnline === 1 ? 'online' : 'offline'}"></span> <span class="server-name">${server.serverName}</span> </div> <span class="player-count"><span class="num">${server.playerCount}</span> / ${server.maxPlayer}${queueText}</span> </div> ${featuresHTML ? '<div class="features">' + featuresHTML + '</div>' : ''} </div> <div class="chart-area"> ${server.isOnline === 1 && server.playerHistory && server.playerHistory.length > 0 ? (() => { const normalized = normalizeHistory(server.playerHistory); return normalized.length > 0 ? buildChartSVG(normalized, 380, 38, server.maxPlayer) : ''; })() : ''} </div>`; return card; } /** * 主渲染函数 */ function setData(apiData) { if (!apiData || !apiData.data) return; const servers = apiData.data; const listEl = document.getElementById('server-list'); listEl.innerHTML = ''; servers.forEach(server => { listEl.appendChild(createServerCard(server)); }); } </script> </body> </html>