feat(public): enhance system metrics display and user interface

- Added a main title to the index.html for better context.
- Introduced a new stats container to organize summary and detail statistics.
- Implemented a refresh animation for loading stats, improving user experience.
- Updated the updateStats function to streamline data handling and display.
- Enhanced the metrics section with improved formatting and error handling.
- Refined CSS styles for better visual presentation and responsiveness.
- Ensured a cleaner layout for system metrics and top referers, contributing to a more intuitive interface.
This commit is contained in:
wood chen 2024-12-01 02:47:23 +08:00
parent 454fe7380f
commit 6a0df0bdc7
3 changed files with 400 additions and 158 deletions

View File

@ -3,9 +3,22 @@ body {
height: 100%;
margin: 0;
font-weight: 300;
background: transparent;
overflow: auto;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(https://random-api.czl.net/pic/all);
background-size: cover;
overflow: auto;
background-position: center;
z-index: -1;
opacity: 0.8;
}
.overlay {
@ -14,7 +27,7 @@ body {
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
background-color: rgba(0, 0, 0, 0.7);
z-index: 2;
overflow-y: auto;
}
@ -45,29 +58,27 @@ img {
}
.stats-summary {
background-color: #44444423;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
padding: 20px;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 100%;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
/* 两列布局 */
gap: 10px;
/* 项目之间的间距 */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stats-item {
padding: 5px;
}
/* 确保在小屏幕上切换为单列 */
@media (max-width: 600px) {
.stats-grid {
grid-template-columns: 1fr;
}
background: rgba(255, 255, 255, 0.05);
padding: 12px 15px;
border-radius: 6px;
font-size: 0.95em;
color: #999;
}
.stats-header {
@ -80,6 +91,7 @@ img {
.stats-header h2 {
margin: 0;
padding: 0;
color: #fff;
}
.refresh-icon {
@ -113,21 +125,16 @@ table {
} */
.endpoint-link {
/* color: #666; 默认字体颜色改为深灰色 */
text-decoration: none;
cursor: pointer;
padding: 2px 8px; /* 稍微增加内边距 */
color: #2196f3;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.3s ease; /* 平滑过渡效果 */
display: inline-block; /* 使padding生效 */
font-weight: 500; /* 稍微加粗 */
transition: all 0.2s ease;
}
.endpoint-link:hover {
background-color: #2196f3; /* 鼠标悬停时的背景色改为蓝色 */
color: white; /* 鼠标悬停时文字变为白色 */
transform: translateY(-1px); /* 轻微上浮效果 */
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.2); /* 添加阴影效果 */
background: rgba(33, 150, 243, 0.1);
color: #2196f3;
transform: translateY(-1px);
}
/* 点击时的效果 */
@ -176,78 +183,185 @@ table {
/* 系统监控样式 */
.metrics-container {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.metrics-section {
margin-bottom: 20px;
margin-bottom: 20px;
}
.metrics-section h3 {
color: #2196f3;
margin-bottom: 15px;
font-size: 1.1em;
border-bottom: 1px solid rgba(33, 150, 243, 0.2);
padding-bottom: 5px;
color: #2196f3;
margin-bottom: 15px;
font-size: 1.1em;
border-bottom: 1px solid rgba(33, 150, 243, 0.2);
padding-bottom: 5px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.metric-item {
background: rgba(255, 255, 255, 0.1);
padding: 12px;
border-radius: 6px;
font-size: 0.9em;
background: rgba(255, 255, 255, 0.1);
padding: 12px;
border-radius: 6px;
font-size: 0.9em;
}
.status-codes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.status-code-item {
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.recent-requests table {
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
}
.recent-requests th,
.recent-requests td {
padding: 8px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.recent-requests th {
color: #2196f3;
font-weight: 500;
color: #2196f3;
font-weight: 500;
}
.top-referers {
display: grid;
gap: 8px;
display: grid;
gap: 8px;
}
.referer-item {
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.referer {
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.count {
color: #2196f3;
font-weight: 500;
}
/* 更新表格样式 */
.stats-table {
margin-top: 20px;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
overflow: hidden;
margin: 20px 0;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: #999;
}
th {
background: rgba(33, 150, 243, 0.1);
font-weight: 500;
color: #2196f3;
}
tr:hover {
background: rgba(255, 255, 255, 0.05);
}
/* 操作按钮样式 */
td a {
color: #2196f3;
text-decoration: none;
margin-right: 8px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
td a:hover {
background: rgba(33, 150, 243, 0.1);
}
/* 响应式优化 */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: 1fr;
}
th, td {
padding: 8px 10px;
}
.stats-table {
margin: 10px -15px;
width: calc(100% + 30px);
}
}
/* 系统指标样式 */
.metrics-section {
margin-bottom: 20px;
}
.metric-label {
color: #999;
font-size: 0.9em;
margin-bottom: 4px;
}
.metric-value {
font-size: 1.1em;
font-weight: 500;
color: #2196f3;
}
.referers-list {
margin-top: 15px;
display: grid;
gap: 8px;
width: 100%;
}
.referer-item {
background: rgba(255, 255, 255, 0.05);
padding: 10px 15px;
border-radius: 6px;
display: flex;
justify-content: space-between;
@ -255,6 +369,7 @@ table {
}
.referer {
color: #999;
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
@ -264,4 +379,64 @@ table {
.count {
color: #2196f3;
font-weight: 500;
padding: 2px 8px;
background: rgba(33, 150, 243, 0.1);
border-radius: 4px;
}
.error-message {
background: rgba(255, 0, 0, 0.1);
color: #ff4444;
padding: 12px;
border-radius: 6px;
margin: 10px 0;
text-align: center;
}
/* 确保系统指标和统计数据之间有适当间距 */
#system-metrics {
max-width: 800px;
margin: 0 auto 30px auto;
}
/* 优化移动端显示 */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: 1fr;
}
.metric-label {
font-size: 0.85em;
}
.metric-value {
font-size: 1em;
}
.referer {
max-width: 60%;
}
}
.main-title {
text-align: center;
color: #fff;
margin: 20px 0;
font-size: 2em;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 3;
}
/* 修改统计数据容器的宽度限制 */
.stats-container {
max-width: 800px;
margin: 0 auto;
}
/* 移动端适配 */
@media (max-width: 768px) {
.stats-container {
padding: 0 15px;
}
}

View File

@ -19,9 +19,14 @@
</head>
<body>
<h1 class="main-title">Random-Api 随机文件API</h1>
<div class="overlay">
<main>
<div id="system-metrics"></div>
<div class="stats-container">
<div id="stats-summary"></div>
<div id="stats-detail"></div>
</div>
<div id="markdown-content" class="prose prose-dark">
</div>
</main>
@ -58,6 +63,18 @@
// 加载统计数据
async function loadStats() {
try {
// 添加刷新动画
const refreshIcon = document.querySelector('.refresh-icon');
const summaryElement = document.getElementById('stats-summary');
const detailElement = document.getElementById('stats-detail');
if (refreshIcon) {
refreshIcon.classList.add('spinning');
}
if (summaryElement) summaryElement.classList.add('fade');
if (detailElement) detailElement.classList.add('fade');
// 获取数据
const [statsResponse, urlStatsResponse, endpointConfig] = await Promise.all([
fetch('/stats'),
fetch('/urlstats'),
@ -67,30 +84,25 @@
const stats = await statsResponse.json();
const urlStats = await urlStatsResponse.json();
// 只显示 endpoint.json 中配置的路径
const filteredPaths = {};
for (const [category, types] of Object.entries(endpointConfig)) {
if (urlStats.paths[category]) {
filteredPaths[category] = {};
for (const [type, desc] of Object.entries(types)) {
if (urlStats.paths[category][type]) {
filteredPaths[category][type] = {
path: urlStats.paths[category][type],
description: desc
};
}
}
}
}
// 更新统计
await updateStats(stats, urlStats);
// 移除动画
setTimeout(() => {
if (refreshIcon) {
refreshIcon.classList.remove('spinning');
}
if (summaryElement) summaryElement.classList.remove('fade');
if (detailElement) detailElement.classList.remove('fade');
}, 300);
await updateStats(stats, filteredPaths);
} catch (error) {
console.error('Error loading stats:', error);
}
}
// 更新统计显示
async function updateStats(stats, paths) {
async function updateStats(stats, urlStats) {
const startDate = new Date('2024-11-1');
const today = new Date();
const daysSinceStart = Math.ceil((today - startDate) / (1000 * 60 * 60 * 24));
@ -106,6 +118,9 @@
const avgCallsPerDay = Math.round(totalCalls / daysSinceStart);
// 获取 endpoint 配置
const endpointConfig = await loadEndpointConfig();
// 更新总览统计
const summaryHtml = `
<div class="stats-summary">
@ -119,13 +134,6 @@
<div class="stats-item">统计开始日期2024-11-1</div>
</div>
</div>
`;
// 获取 endpoint 配置
const endpointConfig = await loadEndpointConfig();
// 生成详细统计表格
let detailHtml = `
<table>
<thead>
<tr>
@ -137,48 +145,40 @@
</tr>
</thead>
<tbody>
`;
// 按 order 排序并生成表格行
const endpoints = Object.entries(endpointConfig)
.sort(([, a], [, b]) => a.order - b.order);
for (const [endpoint, config] of endpoints) {
const stat = stats[endpoint] || { today_calls: 0, total_calls: 0 };
const urlCount = urlStats[endpoint]?.total_urls || 0;
detailHtml += `
<tr>
<td>
<a href="javascript:void(0)"
onclick="copyToClipboard('${endpoint}')"
class="endpoint-link"
title="点击复制链接">
${config.name}
</a>
</td>
<td>${stat.today_calls}</td>
<td>${stat.total_calls}</td>
<td>${urlCount}</td>
<td>
<a href="/${endpoint}" target="_blank" rel="noopener noreferrer" title="测试接口">👀</a>
<a href="javascript:void(0)" onclick="copyToClipboard('${endpoint}')" title="复制链接">📋</a>
</td>
</tr>
`;
}
detailHtml += `
${Object.entries(endpointConfig)
.sort(([, a], [, b]) => (a.order || 0) - (b.order || 0))
.map(([endpoint, config]) => {
const stat = stats[endpoint] || { today_calls: 0, total_calls: 0 };
const urlCount = urlStats[endpoint]?.total_urls || 0;
return `
<tr>
<td>
<a href="javascript:void(0)"
onclick="copyToClipboard('${endpoint}')"
class="endpoint-link"
title="点击复制链接">
${config.name}
</a>
</td>
<td>${stat.today_calls}</td>
<td>${stat.total_calls}</td>
<td>${urlCount}</td>
<td>
<a href="/${endpoint}" target="_blank" rel="noopener noreferrer" title="测试接口">👀</a>
<a href="javascript:void(0)" onclick="copyToClipboard('${endpoint}')" title="复制链接">📋</a>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
// 更新 DOM
const summaryElement = document.getElementById('stats-summary');
const detailElement = document.getElementById('stats-detail');
if (summaryElement) summaryElement.innerHTML = summaryHtml;
if (detailElement) detailElement.innerHTML = detailHtml;
const container = document.querySelector('.stats-container');
if (container) {
container.innerHTML = summaryHtml;
}
}
// 复制链接功能
@ -213,43 +213,113 @@
const response = await fetch('/metrics');
const data = await response.json();
// 添加数据验证
if (!data || typeof data !== 'object') {
throw new Error('Invalid metrics data received');
}
// 格式化函数
const formatUptime = (ns) => {
const seconds = Math.floor(ns / 1e9);
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${days}天 ${hours}小时 ${minutes}分钟`;
};
const formatBytes = (bytes) => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
};
const formatDate = (dateStr) => {
const date = new Date(dateStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
};
updateMetricsDisplay(data);
const metricsHtml = `
<div class="metrics-section">
<div class="stats-summary">
<div class="stats-header">
<h2>💻 系统状态</h2>
</div>
<div class="stats-grid">
<div class="stats-item">
<div class="metric-label">运行时间</div>
<div class="metric-value">${formatUptime(data.uptime)}</div>
</div>
<div class="stats-item">
<div class="metric-label">启动时间</div>
<div class="metric-value">${formatDate(data.start_time)}</div>
</div>
<div class="stats-item">
<div class="metric-label">CPU核心数</div>
<div class="metric-value">${data.num_cpu} 核</div>
</div>
<div class="stats-item">
<div class="metric-label">Goroutine数量</div>
<div class="metric-value">${data.num_goroutine}</div>
</div>
<div class="stats-item">
<div class="metric-label">平均延迟</div>
<div class="metric-value">${data.average_latency.toFixed(2)} ms</div>
</div>
<div class="stats-item">
<div class="metric-label">堆内存分配</div>
<div class="metric-value">${formatBytes(data.memory_stats.heap_alloc)}</div>
</div>
<div class="stats-item">
<div class="metric-label">系统内存</div>
<div class="metric-value">${formatBytes(data.memory_stats.heap_sys)}</div>
</div>
</div>
</div>
${Object.keys(data.top_referers).length > 0 ? `
<div class="stats-summary">
<div class="stats-header">
<h2>🔗 访问来源</h2>
</div>
<div class="referers-list">
${Object.entries(data.top_referers)
.sort(([,a], [,b]) => b - a)
.map(([referer, count]) => `
<div class="referer-item">
<span class="referer">${referer || '直接访问'}</span>
<span class="count">${count}</span>
</div>
`).join('')}
</div>
</div>
` : ''}
</div>
`;
const container = document.getElementById('system-metrics');
if (container) {
container.innerHTML = metricsHtml;
}
} catch (error) {
console.error('Error loading metrics:', error);
document.getElementById('metrics-container').innerHTML =
'<p class="error">加载指标数据时出错,请稍后重试</p>';
const container = document.getElementById('system-metrics');
if (container) {
container.innerHTML = '<div class="error-message">加载系统指标失败</div>';
}
}
}
function updateMetricsDisplay(metricsData) {
// 确保 metricsData 是有效对象
if (!metricsData || typeof metricsData !== 'object') {
console.error('Invalid metrics data');
return;
}
const container = document.getElementById('metrics-container');
container.innerHTML = ''; // 清空现有内容
// 使用 Object.entries 之前进行安全检查
const entries = Object.entries(metricsData).filter(([key, value]) => key && value !== undefined);
entries.forEach(([key, value]) => {
const metricDiv = document.createElement('div');
metricDiv.className = 'metric-item';
metricDiv.innerHTML = `
<h3>${escapeHtml(key)}</h3>
<p>${escapeHtml(String(value))}</p>
`;
container.appendChild(metricDiv);
});
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")

View File

@ -1,15 +1,12 @@
# Random-Api 随机文件API
<div id="system-metrics"></div>
<div class="stats-container">
<div id="stats-summary"></div>
<div id="stats-detail"></div>
</div>
<div id="system-metrics"></div>
---
## 部署和原理
请见我的帖子:[https://q58.org/t/topic/127](https://q58.org/t/topic/127)