DEV-004: M-DEVBOARD环节5 真实API对接+离线降级

This commit is contained in:
zhizhi200271 2026-03-12 20:21:48 +08:00
parent 93fc07c4c3
commit b450cd3a3d
5 changed files with 343 additions and 109 deletions

View File

@ -0,0 +1,81 @@
// =====================================
// HoloLake DevBoard · API Init v1.0
// 负责:异步加载 + 加载动画 + 自动刷新
// 必须在 api.js 和 main.js 之后加载
// =====================================
(function(){
// -------------- 加载动画 --------------
function showLoader(show) {
var el = document.getElementById('devboard-loader');
if (!el && show) {
el = document.createElement('div');
el.id = 'devboard-loader';
el.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,14,23,0.92);display:flex;align-items:center;justify-content:center;z-index:10000';
el.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;gap:16px;"><div style="width:40px;height:40px;border:3px solid rgba(96,165,250,0.3);border-top-color:#60a5fa;border-radius:50%;animation:dbspin 0.8s linear infinite;"></div><div style="font-size:14px;color:#60a5fa;">数据加载中...</div></div>';
var style = document.createElement('style');
style.textContent = '@keyframes dbspin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
document.body.appendChild(el);
}
if (el) el.style.display = show ? 'flex' : 'none';
}
// -------------- 数据加载+渲染 --------------
async function loadAndRender() {
try {
var developers = await apiGetDevelopers();
var stats = await apiGetStats();
var leaderboard = await apiGetLeaderboard();
// 使用 components.js 里实际存在的函数
if (typeof renderDeveloperCards === 'function') {
renderDeveloperCards(developers);
}
if (typeof renderRanking === 'function') {
renderRanking(leaderboard);
}
// stats 暂时用控制台输出,等找到实际渲染函数再改
console.log('[DevBoard] 统计数据:', stats);
console.log('[DevBoard] 数据加载完成,共 ' + developers.length + ' 位开发者');
} catch (err) {
console.error('[DevBoard] 数据加载异常: ', err);
}
}
// -------------- 初始化入口 --------------
async function init() {
showLoader(true);
// 检测API状态
var online = await apiHealthCheck();
showApiStatus(online);
console.log('[DevBoard] API状态: ' + (online ? '在线(实时)' : '离线(降级)'));
// 加载+渲染
await loadAndRender();
showLoader(false);
// 自动刷新仅API在线时
if (online && API.REFRESH_MS > 0) {
console.log('[DevBoard] 自动刷新已启动,间隔 ' + (API.REFRESH_MS/1000) + ' 秒');
setInterval(async function() {
try {
await loadAndRender();
} catch (e) {
console.warn('[DevBoard] 自动刷新失败: ', e.message);
}
}, API.REFRESH_MS);
}
}
// 等待DOM和所有脚本加载完成后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -0,0 +1,75 @@
// =====================================
// HoloLake DevBoard · API Init v1.0
// 负责:异步加载 + 加载动画 + 自动刷新
// 必须在 api.js 和 main.js 之后加载
// =====================================
(function(){
// -------------- 加载动画 --------------
function showLoader(show) {
var el = document.getElementById('devboard-loader');
if (!el && show) {
el = document.createElement('div');
el.id = 'devboard-loader';
el.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,14,23,0.92);display:flex;align-items:center;justify-content:center;z-index:10000';
el.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;gap:16px;"><div style="width:40px;height:40px;border:3px solid rgba(96,165,250,0.3);border-top-color:#60a5fa;border-radius:50%;animation:dbspin 0.8s linear infinite;"></div><div style="font-size:14px;color:#60a5fa;">数据加载中...</div></div>';
var style = document.createElement('style');
style.textContent = '@keyframes dbspin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
document.body.appendChild(el);
}
if (el) el.style.display = show ? 'flex' : 'none';
}
// -------------- 数据加载+渲染 --------------
async function loadAndRender() {
try {
var developers = await apiGetDevelopers();
var stats = await apiGetStats();
var leaderboard = await apiGetLeaderboard();
// 用现有渲染函数main.js / components.js 定义的)
if (typeof renderDeveloperCards === 'function') renderDeveloperCards(developers);
if (typeof renderStats === 'function') renderStats(stats);
if (typeof renderLeaderboard === 'function') renderLeaderboard(leaderboard);
if (typeof renderRankings === 'function') renderRankings(leaderboard);
console.log('[DevBoard] 数据加载完成,共 ' + developers.length + ' 位开发者');
} catch (err) {
console.error('[DevBoard] 数据加载异常: ', err);
}
}
// -------------- 初始化入口 --------------
async function init() {
showLoader(true);
// 检测API状态
var online = await apiHealthCheck();
showApiStatus(online);
console.log('[DevBoard] API状态: ' + (online ? '在线(实时)' : '离线(降级)'));
// 加载+渲染
await loadAndRender();
showLoader(false);
// 自动刷新仅API在线时
if (online && API.REFRESH_MS > 0) {
console.log('[DevBoard] 自动刷新已启动,间隔 ' + (API.REFRESH_MS/1000) + ' 秒');
setInterval(async function() {
try {
await loadAndRender();
} catch (e) {
console.warn('[DevBoard] 自动刷新失败: ', e.message);
}
}, API.REFRESH_MS);
}
}
// 等待DOM和所有脚本加载完成后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -1,79 +1,122 @@
// api.js - API配置和模拟数据切换
// ======================
// HoloLake DevBoard · API Client v2.0
// DEV-004 之之 · 环节5 · 真实API对接
// 协议SYSLOG-v4.0
// ======================
const CONFIG = {
// API基础地址页页的接口
BASE_URL: 'https://guanghulab.com/api',
// 是否使用真实APIfalse = 使用模拟数据)
USE_REAL_API: false,
// API端点
endpoints: {
teamStatus: '/dashboard/team-status',
developerDetail: '/dashboard/dev/', // + id
modules: '/dashboard/modules'
}
const API = {
// ====== 配置区 ======
// 页页的后端API地址如果地址不对问知秋确认
BASE_URL: 'https://guanghulab.com/api/devboard',
TIMEOUT: 8000,
FALLBACK: true, // API挂了自动切换模拟数据
REFRESH_MS: 30000, // 30秒自动刷新
_cache: {}
};
// 检查API是否可用
async function checkApiAvailability() {
if (!CONFIG.USE_REAL_API) return false;
// ====== 通用请求(带超时+缓存+降级) =====================
async function apiFetch(path) {
const url = API.BASE_URL + path;
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), API.TIMEOUT);
try {
const response = await fetch(`${CONFIG.BASE_URL}/health`, {
method: 'HEAD',
mode: 'cors',
cache: 'no-cache'
const res = await fetch(url, {
signal: ctrl.signal,
headers: { 'Accept': 'application/json' }
});
return response.ok;
} catch (error) {
console.log('API不可用使用模拟数据');
clearTimeout(timer);
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
API._cache[path] = { data: data, time: Date.now() };
return data;
} catch (err) {
clearTimeout(timer);
console.warn(' 🚨 [DevBoard API] ' + path + ' 失败: ', err.message);
if (API._cache[path]) {
console.info(' 💾 [DevBoard API] 使用缓存');
return API._cache[path].data;
}
if (API.FALLBACK) {
console.info(' 🛡️ [DevBoard API] 降级到模拟数据');
return getMockData(path);
}
throw err;
}
}
// ========== 公开接口 ==========
async function apiGetDevelopers() {
return apiFetch('/developers');
}
async function apiGetDeveloperDetail(devId) {
return apiFetch('/developers/' + devId);
}
async function apiGetStats() {
return apiFetch('/stats');
}
async function apiGetLeaderboard() {
return apiFetch('/leaderboard');
}
// ========== 健康检查 ==========
async function apiHealthCheck() {
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 3000);
const res = await fetch(API.BASE_URL + '/health', { signal: ctrl.signal });
clearTimeout(t);
return res.ok;
} catch (e) {
return false;
}
}
// 获取团队状态
async function getTeamStatus() {
if (CONFIG.USE_REAL_API) {
try {
const response = await fetch(`${CONFIG.BASE_URL}${CONFIG.endpoints.teamStatus}`);
if (response.ok) {
const data = await response.json();
return data;
}
} catch (error) {
console.warn('API请求失败使用模拟数据');
}
// ========== 模拟数据(降级保护) ==========
function getMockData(path) {
var devs = [
{ dev_id:'DEV-001', name:'页页', module:'BC-集成', status:'进行中', streak:5, el:'EL-6', pca:{EXE:75,TEC:60,SYS:70,COL:80,INI:65} },
{ dev_id:'DEV-002', name:'肥猫', module:'M-STATUS', status:'进行中', streak:9, el:'EL-5', pca:{EXE:72,TEC:40,SYS:68,COL:82,INI:70} },
{ dev_id:'DEV-003', name:'燕樊', module:'M-MEMORY', status:'进行中', streak:7, el:'EL-5', pca:{EXE:70,TEC:55,SYS:65,COL:75,INI:68} },
{ dev_id:'DEV-004', name:'之之', module:'M-DEVBOARD', status:'进行中', streak:14, el:'EL-8', pca:{EXE:90,TEC:55,SYS:80,COL:85,INI:88} },
{ dev_id:'DEV-009', name:'花尔', module:'M20', status:'进行中', streak:4, el:'EL-8', pca:{EXE:65,TEC:45,SYS:55,COL:70,INI:60} },
{ dev_id:'DEV-010', name:'桔子', module:'M-CHANNEL', status:'进行中', streak:14, el:'EL-6', pca:{EXE:85,TEC:50,SYS:75,COL:80,INI:82} },
{ dev_id:'DEV-011', name:'匆匆那年',module:'M16', status:'等待中', streak:1, el:'EL-3', pca:{EXE:50,TEC:30,SYS:40,COL:55,INI:45} },
{ dev_id:'DEV-012', name:'Awen', module:'M22', status:'进行中', streak:13, el:'EL-6', pca:{EXE:88,TEC:45,SYS:78,COL:90,INI:80} },
{ dev_id:'DEV-013', name:'小兴', module:'M-AUTH', status:'进行中', streak:1, el:'EL-3', pca:{EXE:55,TEC:25,SYS:35,COL:60,INI:50} }
];
var stats = { total_developers:10, active:8, modules:12, code_lines:32000, longest_streak:14, avg_streak:7.5 };
if (path === '/developers') return devs;
if (path.indexOf('/developers/') === 0) {
var id = path.split('/').pop();
return devs.find(function(d){ return d.dev_id === id; }) || devs[0];
}
// 返回模拟数据
return {
developers: window.getDevelopers ? window.getDevelopers() : []
};
if (path === '/stats') return stats;
if (path === '/leaderboard') return devs.slice().sort(function(a,b){ return b.streak - a.streak; });
return [];
}
// 获取开发者详情
async getDeveloperDetail(devId) {
if (CONFIG.USE_REAL_API) {
try {
const response = await fetch(`${CONFIG.BASE_URL}${CONFIG.endpoints.developerDetail}${devId}`);
if (response.ok) {
const data = await response.json();
return data;
}
} catch (error) {
console.warn('API请求失败使用模拟数据');
}
// ========== API状态指示器 ==========
function showApiStatus(online) {
var el = document.getElementById('api-status');
if (!el) {
el = document.createElement('div');
el.id = 'api-status';
el.style.cssText = 'position:fixed;bottom:12px;right:12px;padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600;z-index:9999;transition:all 0.3s;';
document.body.appendChild(el);
}
if (online) {
el.textContent = '🟢 实时数据';
el.style.backgroundColor = 'rgba(52,211,153,0.15)';
el.style.color = '#34d399';
el.style.border = '1px solid rgba(52,211,153,0.3)';
} else {
el.textContent = '🟡 模拟数据';
el.style.backgroundColor = 'rgba(251,191,36,0.15)';
el.style.color = '#fbbf24';
el.style.border = '1px solid rgba(251,191,36,0.3)';
}
// 返回模拟数据
return window.getDeveloperById ? window.getDeveloperById(devId) : null;
}
// 导出配置
window.API = {
CONFIG,
checkApiAvailability,
getTeamStatus,
getDeveloperDetail
};

View File

@ -1,53 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<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>开发者实时看板 · 环节0~4 · 之之</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="components.css">
<link rel="stylesheet" href="detail.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者实时看板 · HoloLake</title>
<style>
/* 这里保留你原有的样式,如果样式比较多,建议去复制原有的内容 */
/* 为了保持简洁,秋秋只放必须的样式框架,妈妈根据实际补充 */
body {
background: #0a0e17;
color: #e1e5ee;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
}
/* 其他样式保持不变 */
</style>
</head>
<body>
<!-- 总览页容器 -->
<div id="overview-container" class="overview-grid fade-in">
<header class="dashboard-header">
<h1>⚡ 开发者实时看板 ⚡</h1>
<p>DEV-004 · 之之 · 十三连胜追平桔子 · EL-6</p>
</header>
<!-- 搜索和筛选区 -->
<div class="controls-section">
<input type="text" id="search-input" placeholder="🔍 搜索开发者姓名/ID/模块..." class="search-box">
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">全部</button>
<button class="filter-btn" data-filter="active">进行中</button>
<button class="filter-btn" data-filter="completed">已完成</button>
</div>
</div>
<!-- 开发者卡片网格 -->
<div id="developers-grid" class="developers-grid"></div>
<!-- 雷达图+排行榜区 -->
<div class="stats-section">
<div class="radar-container">
<h2>团队PCA雷达</h2>
<canvas id="team-radar" width="400" height="300"></canvas>
</div>
<div class="ranking-container">
<h2>连胜排行榜</h2>
<div id="ranking-list"></div>
</div>
</div>
</div>
<h1>🚀 HoloLake 开发者实时看板</h1>
<div id="stats-container"></div>
<div id="developers-container"></div>
<div id="leaderboard-container"></div>
<!-- 详情页容器 -->
<div id="detail-container" style="display: none;"></div>
<script src="radar.js"></script>
<script src="components.js"></script>
<script src="detail.js"></script>
<!-- ⚠️ 重点:脚本引入顺序必须正确! -->
<!-- 1. API客户端最先加载 -->
<script src="api.js"></script>
<!-- 2. 然后是原有的业务逻辑脚本 -->
<script src="main.js"></script>
<script src="components.js"></script>
<!-- 3. 初始化引擎最后加载 -->
<script src="api-init.js"></script>
</body>
</html>

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes">
<title>开发者实时看板 · 环节0~4 · 之之</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="components.css">
<link rel="stylesheet" href="detail.css">
</head>
<body>
<!-- 总览页容器 -->
<div id="overview-container" class="overview-grid fade-in">
<header class="dashboard-header">
<h1>⚡ 开发者实时看板 ⚡</h1>
<p>DEV-004 · 之之 · 十三连胜追平桔子 · EL-6</p>
</header>
<!-- 搜索和筛选区 -->
<div class="controls-section">
<input type="text" id="search-input" placeholder="🔍 搜索开发者姓名/ID/模块..." class="search-box">
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">全部</button>
<button class="filter-btn" data-filter="active">进行中</button>
<button class="filter-btn" data-filter="completed">已完成</button>
</div>
</div>
<!-- 开发者卡片网格 -->
<div id="developers-grid" class="developers-grid"></div>
<!-- 雷达图+排行榜区 -->
<div class="stats-section">
<div class="radar-container">
<h2>团队PCA雷达</h2>
<canvas id="team-radar" width="400" height="300"></canvas>
</div>
<div class="ranking-container">
<h2>连胜排行榜</h2>
<div id="ranking-list"></div>
</div>
</div>
</div>
<!-- 详情页容器 -->
<div id="detail-container" style="display: none;"></div>
<script src="radar.js"></script>
<script src="components.js"></script>
<script src="detail.js"></script>
<script src="main.js"></script>
</body>
</html>