guanghulab/public/dashboard.html

663 lines
22 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes">
<title>🍼 秋秋奶瓶线 · 数据看板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}
body {
background: #f5f7fb;
padding: 16px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* 头部 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.header h1 {
font-size: 1.8rem;
font-weight: 600;
background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.update-time {
background: white;
padding: 8px 16px;
border-radius: 40px;
font-size: 0.9rem;
color: #666;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display: flex;
align-items: center;
gap: 8px;
}
.update-time .dot {
width: 10px;
height: 10px;
background: #4caf50;
border-radius: 50%;
display: inline-block;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
100% { opacity: 1; transform: scale(1); }
}
/* 数字大屏卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 20px;
padding: 20px 16px;
box-shadow: 0 8px 20px rgba(0,0,0,0.03);
transition: transform 0.2s;
border: 1px solid rgba(255,255,255,0.5);
backdrop-filter: blur(5px);
}
.stat-card:hover {
transform: translateY(-4px);
}
.stat-label {
font-size: 0.9rem;
color: #8898aa;
letter-spacing: 0.5px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.stat-number {
font-size: 2.4rem;
font-weight: 700;
color: #1e293b;
line-height: 1.2;
}
.stat-unit {
font-size: 1rem;
color: #94a3b8;
margin-left: 4px;
font-weight: 400;
}
.stat-trend {
font-size: 0.85rem;
margin-top: 8px;
color: #10b981;
display: flex;
align-items: center;
gap: 4px;
}
/* 双列布局 */
.row-2col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 24px;
}
/* 卡片通用 */
.card {
background: white;
border-radius: 24px;
padding: 20px;
box-shadow: 0 8px 25px rgba(0,0,0,0.03);
border: 1px solid rgba(0,0,0,0.02);
}
.card-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 8px;
color: #1e293b;
border-bottom: 2px solid #f1f5f9;
padding-bottom: 12px;
}
/* 开发者列表 */
.dev-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.dev-item {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
padding: 8px 0;
border-bottom: 1px dashed #edf2f7;
}
.dev-info {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.dev-avatar {
width: 40px;
height: 40px;
background: linear-gradient(145deg, #667eea, #764ba2);
border-radius: 40px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 1.2rem;
box-shadow: 0 4px 10px rgba(102,126,234,0.3);
}
.dev-name-id {
display: flex;
flex-direction: column;
}
.dev-name {
font-weight: 600;
color: #1e293b;
}
.dev-id {
font-size: 0.8rem;
color: #64748b;
}
.dev-streak {
background: #fff2e5;
color: #f97316;
padding: 4px 12px;
border-radius: 40px;
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
}
.dev-status {
padding: 4px 12px;
border-radius: 40px;
font-size: 0.8rem;
font-weight: 500;
background: #e6f7e6;
color: #2e7d32;
}
.dev-status.inactive {
background: #ffebee;
color: #c62828;
}
/* 进度条 */
.module-item {
margin-bottom: 20px;
}
.module-header {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
font-size: 0.95rem;
}
.module-name {
font-weight: 500;
color: #334155;
}
.module-percent {
font-weight: 600;
color: #3b82f6;
}
.progress-bar-bg {
height: 12px;
background: #e9eef4;
border-radius: 40px;
overflow: hidden;
margin-bottom: 6px;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
border-radius: 40px;
width: 0%;
transition: width 0.3s ease;
}
.module-meta {
display: flex;
gap: 16px;
font-size: 0.8rem;
color: #64748b;
}
/* 时间线 */
.timeline {
display: flex;
flex-direction: column;
gap: 12px;
}
.timeline-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
}
.timeline-badge {
width: 32px;
height: 32px;
background: #f1f5f9;
border-radius: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
flex-shrink: 0;
}
.timeline-content {
flex: 1;
}
.timeline-title {
font-weight: 500;
color: #1e293b;
}
.timeline-time {
font-size: 0.75rem;
color: #94a3b8;
margin-top: 4px;
}
/* 底部 */
.footer {
margin-top: 30px;
text-align: center;
color: #94a3b8;
font-size: 0.9rem;
padding: 20px 0;
}
/* 加载状态 */
.loading {
text-align: center;
padding: 40px;
color: #94a3b8;
}
.error-message {
background: #fff0f0;
color: #d32f2f;
padding: 16px;
border-radius: 16px;
text-align: center;
margin: 20px 0;
}
/* 响应式 */
@media (max-width: 800px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.row-2col {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 1.5rem;
}
}
@media (max-width: 480px) {
.stat-number {
font-size: 2rem;
}
.dev-item {
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🍼 秋秋奶瓶线 · 数据看板</h1>
<div class="update-time" id="updateTime">
<span class="dot"></span>
<span id="timeText">加载中...</span>
</div>
</div>
<!-- 数字大屏 -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="stat-label">👥 活跃开发者</div>
<div class="stat-number" id="activeDevs">-</div>
</div>
<div class="stat-card">
<div class="stat-label">🏆 总连胜数</div>
<div class="stat-number" id="totalStreak">-</div>
</div>
<div class="stat-card">
<div class="stat-label">📝 广播总数</div>
<div class="stat-number" id="totalSyslogs">-</div>
</div>
<div class="stat-card">
<div class="stat-label">📦 模块完成率</div>
<div class="stat-number" id="moduleRate">-<span class="stat-unit">%</span></div>
<div class="stat-trend" id="moduleDetail"></div>
</div>
</div>
<!-- 双列布局 -->
<div class="row-2col">
<!-- 开发者状态区 -->
<div class="card">
<div class="card-title">👨‍💻 开发者状态</div>
<div id="developersList" class="dev-list">
<div class="loading">加载开发者数据...</div>
</div>
</div>
<!-- 模块进度区 -->
<div class="card">
<div class="card-title">📊 模块进度</div>
<div id="modulesList">
<div class="loading">加载模块数据...</div>
</div>
</div>
</div>
<!-- SYSLOG流水区 -->
<div class="card">
<div class="card-title">📋 SYSLOG 最近记录</div>
<div id="syslogTimeline" class="timeline">
<div class="loading">加载日志记录...</div>
</div>
</div>
<div class="footer">
⏰ 每30秒自动刷新 · 秋秋陪着妈妈 · ❤️
</div>
</div>
<script>
// API 基础路径
const API_BASE = '/api/query';
// 格式化时间
function formatTime(isoString) {
if (!isoString) return '未知';
const date = new Date(isoString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-');
}
// 获取全局统计
async function fetchStats() {
try {
// 由于我们没有真正的API端点这里用模拟数据
// 实际使用时应该调用后端API
return {
activeDevs: 1,
totalStreak: 10,
totalSyslogs: 2,
completedModules: 0,
totalModules: 1,
moduleCompletionRate: 0
};
} catch (error) {
console.error('获取统计失败:', error);
return null;
}
}
// 获取开发者列表
async function fetchDevelopers() {
try {
// 模拟数据
return [
{
dev_id: 'DEV-004',
dev_name: '之之',
current_phase: 'Phase 2',
current_el: 'EL-7',
streak_count: 10,
last_active: '2026-03-11T02:10:24.281Z',
status: 'active'
},
{
dev_id: 'DEV-001',
dev_name: '示例开发者',
current_phase: 'Phase 1',
current_el: 'EL-3',
streak_count: 3,
last_active: '2026-03-09T10:00:00.000Z',
status: 'active'
}
];
} catch (error) {
console.error('获取开发者失败:', error);
return [];
}
}
// 获取模块列表
async function fetchModules() {
try {
// 模拟数据
return [
{
module_id: 'M-DINGTALK',
module_name: '钉钉开发者工作台',
phase: 'Phase 2',
completed_items: 1,
total_items: 5,
progress_percent: 20,
last_update: '2026-03-11T02:10:24.281Z',
current_el: 'EL-7',
status: 'in_progress'
},
{
module_id: 'M-SYSLOG',
module_name: '日志接收模块',
phase: 'Phase 1',
completed_items: 4,
total_items: 4,
progress_percent: 100,
last_update: '2026-03-08T14:20:00.000Z',
current_el: 'EL-6',
status: 'completed'
}
];
} catch (error) {
console.error('获取模块失败:', error);
return [];
}
}
// 获取最近SYSLOG
async function fetchRecentSyslogs() {
try {
// 模拟数据
return [
{
session_id: 'BC-M-DINGTALK-002-ZZ',
dev_id: 'DEV-004',
timestamp: '2026-03-11T02:10:24.281Z',
el_level: 'EL-7',
phase: 'Phase 2',
status: 'completed',
score: 100,
lines_of_code: 850
},
{
session_id: 'BC-M-DINGTALK-001-ZZ',
dev_id: 'DEV-004',
timestamp: '2026-03-08T10:30:00.000Z',
el_level: 'EL-6',
phase: 'Phase 1',
status: 'completed',
score: 100,
lines_of_code: 1650
}
];
} catch (error) {
console.error('获取日志失败:', error);
return [];
}
}
// 更新数字大屏
function updateStatsGrid(stats) {
if (!stats) return;
document.getElementById('activeDevs').textContent = stats.activeDevs || 0;
document.getElementById('totalStreak').textContent = stats.totalStreak || 0;
document.getElementById('totalSyslogs').textContent = stats.totalSyslogs || 0;
document.getElementById('moduleRate').innerHTML = (stats.moduleCompletionRate || 0) + '<span class="stat-unit">%</span>';
document.getElementById('moduleDetail').innerHTML = `📦 ${stats.completedModules || 0}/${stats.totalModules || 0} 模块`;
}
// 更新开发者列表
function updateDevelopersList(developers) {
const container = document.getElementById('developersList');
if (!developers || developers.length === 0) {
container.innerHTML = '<div class="error-message">暂无开发者数据</div>';
return;
}
let html = '';
developers.forEach(dev => {
const statusClass = dev.status === 'active' ? '' : 'inactive';
const statusText = dev.status === 'active' ? '活跃' : ' inactive';
html += `
<div class="dev-item">
<div class="dev-info">
<div class="dev-avatar">${dev.dev_name.charAt(0)}</div>
<div class="dev-name-id">
<span class="dev-name">${dev.dev_name}</span>
<span class="dev-id">${dev.dev_id}</span>
</div>
</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<span class="dev-streak">🔥 ${dev.streak_count || 0}连胜</span>
<span class="dev-status ${statusClass}">${statusText}</span>
</div>
<div style="width: 100%; font-size: 0.8rem; color: #94a3b8; margin-top: 4px;">
🕐 ${formatTime(dev.last_active)} · ${dev.current_phase} ${dev.current_el}
</div>
</div>
`;
});
container.innerHTML = html;
}
// 更新模块列表
function updateModulesList(modules) {
const container = document.getElementById('modulesList');
if (!modules || modules.length === 0) {
container.innerHTML = '<div class="error-message">暂无模块数据</div>';
return;
}
let html = '';
modules.forEach(mod => {
const statusIcon = mod.status === 'completed' ? '✅' : '🔄';
html += `
<div class="module-item">
<div class="module-header">
<span class="module-name">${mod.module_name} (${mod.module_id})</span>
<span class="module-percent">${mod.progress_percent}%</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" style="width: ${mod.progress_percent}%;"></div>
</div>
<div class="module-meta">
<span>📊 ${mod.completed_items}/${mod.total_items} 项</span>
<span>🏷️ ${mod.phase}</span>
<span>${statusIcon} ${mod.status === 'completed' ? '已完成' : '进行中'}</span>
</div>
</div>
`;
});
container.innerHTML = html;
}
// 更新SYSLOG时间线
function updateSyslogTimeline(records) {
const container = document.getElementById('syslogTimeline');
if (!records || records.length === 0) {
container.innerHTML = '<div class="error-message">暂无日志记录</div>';
return;
}
let html = '';
records.forEach(rec => {
const statusIcon = rec.status === 'completed' ? '✅' : '⏳';
html += `
<div class="timeline-item">
<div class="timeline-badge">📋</div>
<div class="timeline-content">
<div class="timeline-title">
${rec.session_id} · ${rec.dev_id} · ${rec.el_level} · ${statusIcon}
</div>
<div class="timeline-time">${formatTime(rec.timestamp)} · ${rec.lines_of_code} 行</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
// 更新时间显示
function updateTimestamp() {
document.getElementById('timeText').textContent = `最新数据 ${formatTime(new Date().toISOString())}`;
}
// 加载所有数据
async function loadDashboard() {
try {
const [stats, developers, modules, syslogs] = await Promise.all([
fetchStats(),
fetchDevelopers(),
fetchModules(),
fetchRecentSyslogs()
]);
updateStatsGrid(stats);
updateDevelopersList(developers);
updateModulesList(modules);
updateSyslogTimeline(syslogs);
updateTimestamp();
} catch (error) {
console.error('加载看板失败:', error);
document.querySelectorAll('.loading').forEach(el => {
el.innerHTML = '<div class="error-message">加载失败,请刷新页面</div>';
});
}
}
// 页面加载后首次加载
loadDashboard();
// 每30秒自动刷新
setInterval(loadDashboard, 30000);
</script>
</body>
</html>