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