663 lines
22 KiB
HTML
663 lines
22 KiB
HTML
<!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>
|