DEV-004: M-DEVBOARD环节0-2-开发者实时看板-纯前端-PCA雷达图+排行榜
This commit is contained in:
commit
451a513d1b
|
|
@ -0,0 +1,140 @@
|
|||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(145deg, var(--color-bg-card), #2a3442);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #fff, var(--color-accent-primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.dev-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.dev-card {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 20px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dev-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.dev-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
background: linear-gradient(135deg, var(--color-accent-primary), #4cc9f0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dev-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dev-id {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dev-streak {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.ranking-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 70px;
|
||||
align-items: center;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ranking-streak-bar {
|
||||
background: var(--color-bg-secondary);
|
||||
height: 16px;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.streak-progress {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00b4d8, #4cc9f0);
|
||||
}
|
||||
|
||||
.streak-count {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid var(--color-border);
|
||||
border-top-color: var(--color-accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 2rem;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.navbar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.navbar-logo span {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.navbar-info {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
.sidebar-left, .main-board, .sidebar-right {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.main-container {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
.sidebar-right {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sidebar-left, .main-board, .sidebar-right {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
:root {
|
||||
--color-bg-primary: #0a0e1a;
|
||||
--color-bg-secondary: #111827;
|
||||
--color-bg-card: #1e2433;
|
||||
--color-text-primary: #ffffff;
|
||||
--color-text-secondary: #a0aec0;
|
||||
--color-accent-primary: #00b4d8;
|
||||
--color-accent-secondary: #0077be;
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
--color-border: #2d3748;
|
||||
|
||||
--breakpoint-mobile: 768px;
|
||||
--breakpoint-tablet: 1024px;
|
||||
|
||||
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family-base);
|
||||
background-color: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake 开发者实时看板</title>
|
||||
<link rel="stylesheet" href="css/theme.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar">
|
||||
<div class="navbar-logo">
|
||||
<span>◉</span>
|
||||
<span>HoloLake · DevBoard</span>
|
||||
</div>
|
||||
<div class="navbar-info">
|
||||
<span id="last-update-time">最后更新: --:--:--</span>
|
||||
<span id="refresh-indicator">⏳ 30s</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<aside class="sidebar-left">
|
||||
<h3 style="color: var(--color-accent-primary);">🧠 PCA雷达图</h3>
|
||||
<div style="text-align: center; margin: 0.5rem 0;">
|
||||
<span id="current-dev-name">之之</span> · 点击卡片切换
|
||||
</div>
|
||||
<canvas id="pca-canvas" width="300" height="300" style="width:100%; height:auto;"></canvas>
|
||||
</aside>
|
||||
|
||||
<section class="main-board">
|
||||
<div id="stats-container"></div>
|
||||
<div id="devgrid-container"></div>
|
||||
</section>
|
||||
|
||||
<aside class="sidebar-right">
|
||||
<div id="ranking-container"></div>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/board.js"></script>
|
||||
<script src="js/radar.js"></script>
|
||||
<script src="js/ranking.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
const MOCK_DEVELOPERS = [
|
||||
{ id: 'DEV-001', name: '玄泓', streak: 8, el: 6, pca: { exe: 75, tec: 68, sys: 82, col: 79, ini: 85 }, totalScore: 78, status: 'doing', module: 'M-CORE', lastActive: '2026-03-11T10:30:00Z' },
|
||||
{ id: 'DEV-002', name: '苍钺', streak: 5, el: 5, pca: { exe: 82, tec: 71, sys: 65, col: 90, ini: 72 }, totalScore: 76, status: 'pending', module: 'M-UI', lastActive: '2026-03-11T09:15:00Z' },
|
||||
{ id: 'DEV-003', name: '桔子', streak: 13, el: 8, pca: { exe: 88, tec: 85, sys: 92, col: 78, ini: 90 }, totalScore: 87, status: 'doing', module: 'M-ORCHESTRATOR', lastActive: '2026-03-11T11:20:00Z' },
|
||||
{ id: 'DEV-004', name: '之之', streak: 12, el: 8, pca: { exe: 70, tec: 32, sys: 80, col: 80, ini: 83 }, totalScore: 66, status: 'doing', module: 'M-DEVBOARD', lastActive: '2026-03-11T12:05:00Z' },
|
||||
{ id: 'DEV-005', name: '琉光', streak: 7, el: 6, pca: { exe: 79, tec: 74, sys: 71, col: 88, ini: 77 }, totalScore: 78, status: 'done', module: 'M-TEST', lastActive: '2026-03-10T16:40:00Z' },
|
||||
{ id: 'DEV-006', name: '墨羽', streak: 4, el: 4, pca: { exe: 65, tec: 60, sys: 55, col: 92, ini: 68 }, totalScore: 68, status: 'doing', module: 'M-DOC', lastActive: '2026-03-11T08:50:00Z' },
|
||||
{ id: 'DEV-007', name: '霜砚', streak: 10, el: 7, pca: { exe: 84, tec: 79, sys: 81, col: 75, ini: 82 }, totalScore: 80, status: 'blocked', module: 'M-API', lastActive: '2026-03-11T07:30:00Z' },
|
||||
{ id: 'DEV-008', name: '岚茵', streak: 6, el: 5, pca: { exe: 73, tec: 70, sys: 68, col: 85, ini: 74 }, totalScore: 74, status: 'pending', module: 'M-DESIGN', lastActive: '2026-03-10T14:20:00Z' }
|
||||
];
|
||||
|
||||
function getTopStreak(developers) {
|
||||
if (!developers || developers.length === 0) return { name: '无', count: 0 };
|
||||
let top = developers[0];
|
||||
for (let i = 1; i < developers.length; i++) {
|
||||
if (developers[i].streak > top.streak) {
|
||||
top = developers[i];
|
||||
}
|
||||
}
|
||||
return { name: top.name, count: top.streak };
|
||||
}
|
||||
|
||||
async function getDevStatus() {
|
||||
return MOCK_DEVELOPERS;
|
||||
}
|
||||
|
||||
async function getPCA(devId) {
|
||||
const dev = MOCK_DEVELOPERS.find(d => d.id === devId);
|
||||
if (!dev) return null;
|
||||
return {
|
||||
exe: dev.pca.exe,
|
||||
tec: dev.pca.tec,
|
||||
sys: dev.pca.sys,
|
||||
col: dev.pca.col,
|
||||
ini: dev.pca.ini,
|
||||
total: dev.totalScore,
|
||||
level: getPCAColor(dev.totalScore).level
|
||||
};
|
||||
}
|
||||
|
||||
async function getAllStats() {
|
||||
const activeDevs = MOCK_DEVELOPERS.filter(d => d.status === 'doing').length;
|
||||
const topStreak = getTopStreak(MOCK_DEVELOPERS);
|
||||
return {
|
||||
totalDevs: 8,
|
||||
activeDevs: activeDevs,
|
||||
totalCodeLines: 85600,
|
||||
totalModules: 24,
|
||||
modulesCompleted: 8,
|
||||
modulesInProgress: 12,
|
||||
modulesPending: 4,
|
||||
topStreak: topStreak
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
let refreshInterval;
|
||||
let countdownInterval;
|
||||
let seconds = 30;
|
||||
|
||||
function updateLastUpdateTime() {
|
||||
const el = document.getElementById('last-update-time');
|
||||
if (el) {
|
||||
const now = new Date();
|
||||
el.textContent = `最后更新: ${now.toLocaleTimeString('zh-CN', { hour12: false })}`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
const el = document.getElementById('refresh-indicator');
|
||||
if (el) el.textContent = `⏳ ${seconds}s`;
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
seconds = 30;
|
||||
updateCountdown();
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
countdownInterval = setInterval(() => {
|
||||
seconds--;
|
||||
if (seconds <= 0) seconds = 30;
|
||||
updateCountdown();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function refreshAllData() {
|
||||
const devs = await getDevStatus();
|
||||
const stats = await getAllStats();
|
||||
if (typeof renderStats === 'function') renderStats(stats);
|
||||
if (typeof renderDevCards === 'function') renderDevCards(devs);
|
||||
if (typeof renderRanking === 'function') renderRanking(devs);
|
||||
updateLastUpdateTime();
|
||||
}
|
||||
|
||||
async function initApp() {
|
||||
await initBoard();
|
||||
await initRanking();
|
||||
initRadar();
|
||||
await refreshAllData();
|
||||
startCountdown();
|
||||
if (refreshInterval) clearInterval(refreshInterval);
|
||||
refreshInterval = setInterval(refreshAllData, 30000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initApp);
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (refreshInterval) clearInterval(refreshInterval);
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
});
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
function renderStats(stats) {
|
||||
const container = document.getElementById('stats-container');
|
||||
if (!container) return;
|
||||
container.innerHTML = `
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">活跃开发者</div>
|
||||
<div class="stat-number">${stats.activeDevs}</div>
|
||||
<div class="stat-sub">总人数 ${stats.totalDevs}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总代码量</div>
|
||||
<div class="stat-number">${formatNumber(stats.totalCodeLines)}</div>
|
||||
<div class="stat-sub">行代码</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">模块进度</div>
|
||||
<div class="stat-number">${stats.modulesCompleted}/${stats.totalModules}</div>
|
||||
<div class="stat-sub">✅ ${stats.modulesCompleted} / ⚡ ${stats.modulesInProgress} / ⏳ ${stats.modulesPending}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">最高连胜</div>
|
||||
<div class="stat-number">${stats.topStreak.count}</div>
|
||||
<div class="stat-sub">${stats.topStreak.name} ${getStreakEmoji(stats.topStreak.count)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDevCards(developers) {
|
||||
const container = document.getElementById('devgrid-container');
|
||||
if (!container) return;
|
||||
const sorted = [...developers].sort((a, b) => b.streak - a.streak);
|
||||
let html = '<div class="dev-grid">';
|
||||
sorted.forEach(dev => {
|
||||
const pcaColor = getPCAColor(dev.totalScore);
|
||||
html += `
|
||||
<div class="dev-card" data-dev-id="${dev.id}">
|
||||
<div class="dev-card-header">
|
||||
<div class="dev-avatar">${dev.name[0]}</div>
|
||||
<div class="dev-info">
|
||||
<div class="dev-name">${dev.name}</div>
|
||||
<div class="dev-id">${dev.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dev-module">📁 ${dev.module}</div>
|
||||
<div class="dev-stats">
|
||||
<div class="dev-streak">${dev.streak}<span>连胜</span></div>
|
||||
<div class="dev-badges">
|
||||
<span class="badge badge-el">EL-${dev.el}</span>
|
||||
<span class="badge" style="background:${pcaColor.color}20; color:${pcaColor.color}">${pcaColor.level} · ${dev.totalScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dev-footer">
|
||||
<span>${getStatusBadge(dev.status)}</span>
|
||||
<span>🕒 ${formatDate(dev.lastActive)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function initBoard() {
|
||||
const devs = await getDevStatus();
|
||||
const stats = await getAllStats();
|
||||
renderStats(stats);
|
||||
renderDevCards(devs);
|
||||
}
|
||||
|
||||
window.initBoard = initBoard;
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
function drawRadar(canvasId, pcaData) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
const maxR = Math.min(w, h) * 0.35;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const dims = [
|
||||
{ label: '执行力', val: pcaData.exe || 0 },
|
||||
{ label: '技术纵深', val: pcaData.tec || 0 },
|
||||
{ label: '系统理解', val: pcaData.sys || 0 },
|
||||
{ label: '协作力', val: pcaData.col || 0 },
|
||||
{ label: '主动性', val: pcaData.ini || 0 }
|
||||
];
|
||||
|
||||
for (let lv = 1; lv <= 5; lv++) {
|
||||
const r = (maxR * lv) / 5;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
||||
const x = cx + r * Math.cos(angle);
|
||||
const y = cy + r * Math.sin(angle);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.strokeStyle = '#2d3748';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.lineTo(cx + maxR * Math.cos(angle), cy + maxR * Math.sin(angle));
|
||||
ctx.strokeStyle = '#4a5568';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#cbd5e0';
|
||||
ctx.font = '12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
||||
const x = cx + (maxR + 25) * Math.cos(angle);
|
||||
const y = cy + (maxR + 25) * Math.sin(angle);
|
||||
ctx.fillText(dims[i].label, x, y);
|
||||
}
|
||||
|
||||
const points = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
|
||||
const r = (dims[i].val / 100) * maxR;
|
||||
points.push({
|
||||
x: cx + r * Math.cos(angle),
|
||||
y: cy + r * Math.sin(angle)
|
||||
});
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0].x, points[0].y);
|
||||
for (let i = 1; i < 5; i++) ctx.lineTo(points[i].x, points[i].y);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = 'rgba(0, 180, 216, 0.3)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#00b4d8';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
points.forEach(p => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 4, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#00b4d8';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 24px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(pcaData.total || '0', cx, cy - 10);
|
||||
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
const colorInfo = getPCAColor(pcaData.total || 0);
|
||||
ctx.fillStyle = colorInfo.color;
|
||||
ctx.fillText(pcaData.level || 'C', cx, cy + 15);
|
||||
}
|
||||
|
||||
async function loadRadarForDev(devId) {
|
||||
const pcaData = await getPCA(devId);
|
||||
if (pcaData) drawRadar('pca-canvas', pcaData);
|
||||
}
|
||||
|
||||
function initRadar() {
|
||||
document.querySelectorAll('.dev-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
loadRadarForDev(card.dataset.devId);
|
||||
});
|
||||
});
|
||||
loadRadarForDev('DEV-004');
|
||||
}
|
||||
|
||||
window.initRadar = initRadar;
|
||||
window.loadRadarForDev = loadRadarForDev;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
function renderRanking(developers) {
|
||||
const container = document.getElementById('ranking-container');
|
||||
if (!container) return;
|
||||
const sorted = [...developers].sort((a, b) => b.streak - a.streak);
|
||||
const maxStreak = sorted[0].streak;
|
||||
|
||||
let html = '<div class="ranking-header"><h3>🏆 连胜排行榜</h3><span>实时更新</span></div><div class="ranking-list">';
|
||||
|
||||
sorted.forEach((dev, idx) => {
|
||||
const medal = idx === 0 ? '🥇' : idx === 1 ? '🥈' : idx === 2 ? '🥉' : `${idx+1}.`;
|
||||
const percent = (dev.streak / maxStreak) * 100;
|
||||
html += `
|
||||
<div class="ranking-item" data-dev-id="${dev.id}">
|
||||
<div class="ranking-rank">${medal}</div>
|
||||
<div class="ranking-info">
|
||||
<div class="ranking-name">${dev.name}</div>
|
||||
<div class="ranking-streak-bar">
|
||||
<div class="streak-progress" style="width:${percent}%"></div>
|
||||
<span class="streak-count">${dev.streak}连胜</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function initRanking() {
|
||||
const devs = await getDevStatus();
|
||||
renderRanking(devs);
|
||||
}
|
||||
|
||||
window.initRanking = initRanking;
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
function formatDate(isoString) {
|
||||
if (!isoString) return '--:--:--';
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
|
||||
function getStreakEmoji(count) {
|
||||
if (count >= 20) return '🔥🔥🔥';
|
||||
if (count >= 15) return '🔥🔥';
|
||||
if (count >= 10) return '🔥';
|
||||
if (count >= 5) return '⚡';
|
||||
if (count >= 3) return '👍';
|
||||
return '👣';
|
||||
}
|
||||
|
||||
function getPCAColor(score) {
|
||||
if (score >= 90) return { color: '#FFD700', level: 'S', text: '传奇' };
|
||||
if (score >= 80) return { color: '#00B4D8', level: 'A', text: '精英' };
|
||||
if (score >= 70) return { color: '#10B981', level: 'B', text: '专家' };
|
||||
if (score >= 60) return { color: '#F59E0B', level: 'C', text: '熟手' };
|
||||
return { color: '#EF4444', level: 'D', text: '新手' };
|
||||
}
|
||||
|
||||
function getStatusBadge(status) {
|
||||
const map = {
|
||||
'done': '✅ 已完成',
|
||||
'doing': '⚡ 进行中',
|
||||
'pending': '⏳ 待分配',
|
||||
'blocked': '⚠️ 阻塞'
|
||||
};
|
||||
return map[status] || '⏳ 待分配';
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
return num?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0';
|
||||
}
|
||||
Loading…
Reference in New Issue