commit 451a513d1b7eda5b504e5bf8bb426f9caa827d2a Author: zhizhi200271 Date: Wed Mar 11 20:27:40 2026 +0800 DEV-004: M-DEVBOARD环节0-2-开发者实时看板-纯前端-PCA雷达图+排行榜 diff --git a/modules/devboard/css/components.css b/modules/devboard/css/components.css new file mode 100644 index 00000000..dd88ba47 --- /dev/null +++ b/modules/devboard/css/components.css @@ -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); } +} diff --git a/modules/devboard/css/layout.css b/modules/devboard/css/layout.css new file mode 100644 index 00000000..7811611c --- /dev/null +++ b/modules/devboard/css/layout.css @@ -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; + } +} diff --git a/modules/devboard/css/theme.css b/modules/devboard/css/theme.css new file mode 100644 index 00000000..bf856f51 --- /dev/null +++ b/modules/devboard/css/theme.css @@ -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; +} diff --git a/modules/devboard/index.html b/modules/devboard/index.html new file mode 100644 index 00000000..e9deb195 --- /dev/null +++ b/modules/devboard/index.html @@ -0,0 +1,49 @@ + + + + + + HoloLake 开发者实时看板 + + + + + + + +
+ + +
+
+
+
+ + +
+ + + + + + + + + diff --git a/modules/devboard/js/api.js b/modules/devboard/js/api.js new file mode 100644 index 00000000..88993f09 --- /dev/null +++ b/modules/devboard/js/api.js @@ -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 + }; +} diff --git a/modules/devboard/js/app.js b/modules/devboard/js/app.js new file mode 100644 index 00000000..2184cef9 --- /dev/null +++ b/modules/devboard/js/app.js @@ -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); +}); diff --git a/modules/devboard/js/board.js b/modules/devboard/js/board.js new file mode 100644 index 00000000..1df57d1f --- /dev/null +++ b/modules/devboard/js/board.js @@ -0,0 +1,72 @@ +function renderStats(stats) { + const container = document.getElementById('stats-container'); + if (!container) return; + container.innerHTML = ` +
+
+
活跃开发者
+
${stats.activeDevs}
+
总人数 ${stats.totalDevs}
+
+
+
总代码量
+
${formatNumber(stats.totalCodeLines)}
+
行代码
+
+
+
模块进度
+
${stats.modulesCompleted}/${stats.totalModules}
+
✅ ${stats.modulesCompleted} / ⚡ ${stats.modulesInProgress} / ⏳ ${stats.modulesPending}
+
+
+
最高连胜
+
${stats.topStreak.count}
+
${stats.topStreak.name} ${getStreakEmoji(stats.topStreak.count)}
+
+
+ `; +} + +function renderDevCards(developers) { + const container = document.getElementById('devgrid-container'); + if (!container) return; + const sorted = [...developers].sort((a, b) => b.streak - a.streak); + let html = '
'; + sorted.forEach(dev => { + const pcaColor = getPCAColor(dev.totalScore); + html += ` +
+
+
${dev.name[0]}
+
+
${dev.name}
+
${dev.id}
+
+
+
📁 ${dev.module}
+
+
${dev.streak}连胜
+
+ EL-${dev.el} + ${pcaColor.level} · ${dev.totalScore} +
+
+ +
+ `; + }); + html += '
'; + container.innerHTML = html; +} + +async function initBoard() { + const devs = await getDevStatus(); + const stats = await getAllStats(); + renderStats(stats); + renderDevCards(devs); +} + +window.initBoard = initBoard; diff --git a/modules/devboard/js/radar.js b/modules/devboard/js/radar.js new file mode 100644 index 00000000..49c327c1 --- /dev/null +++ b/modules/devboard/js/radar.js @@ -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; diff --git a/modules/devboard/js/ranking.js b/modules/devboard/js/ranking.js new file mode 100644 index 00000000..fb14624e --- /dev/null +++ b/modules/devboard/js/ranking.js @@ -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 = '

🏆 连胜排行榜

实时更新
'; + + sorted.forEach((dev, idx) => { + const medal = idx === 0 ? '🥇' : idx === 1 ? '🥈' : idx === 2 ? '🥉' : `${idx+1}.`; + const percent = (dev.streak / maxStreak) * 100; + html += ` +
+
${medal}
+
+
${dev.name}
+
+
+ ${dev.streak}连胜 +
+
+
+ `; + }); + html += '
'; + container.innerHTML = html; +} + +async function initRanking() { + const devs = await getDevStatus(); + renderRanking(devs); +} + +window.initRanking = initRanking; diff --git a/modules/devboard/js/utils.js b/modules/devboard/js/utils.js new file mode 100644 index 00000000..a705c934 --- /dev/null +++ b/modules/devboard/js/utils.js @@ -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'; +}