guanghulab/public/dashboard.html
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

663 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>