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