diff --git a/modules/devboard/api.js b/modules/devboard/api.js new file mode 100644 index 00000000..87481710 --- /dev/null +++ b/modules/devboard/api.js @@ -0,0 +1,79 @@ +// api.js - API配置和模拟数据切换 + +const CONFIG = { + // API基础地址(页页的接口) + BASE_URL: 'https://guanghulab.com/api', + + // 是否使用真实API(false = 使用模拟数据) + USE_REAL_API: false, + + // API端点 + endpoints: { + teamStatus: '/dashboard/team-status', + developerDetail: '/dashboard/dev/', // + id + modules: '/dashboard/modules' + } +}; + +// 检查API是否可用 +async function checkApiAvailability() { + if (!CONFIG.USE_REAL_API) return false; + + try { + const response = await fetch(`${CONFIG.BASE_URL}/health`, { + method: 'HEAD', + mode: 'cors', + cache: 'no-cache' + }); + return response.ok; + } catch (error) { + console.log('API不可用,使用模拟数据'); + 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请求失败,使用模拟数据'); + } + } + + // 返回模拟数据 + return { + developers: window.getDevelopers ? window.getDevelopers() : [] + }; +} + +// 获取开发者详情 +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请求失败,使用模拟数据'); + } + } + + // 返回模拟数据 + return window.getDeveloperById ? window.getDeveloperById(devId) : null; +} + +// 导出配置 +window.API = { + CONFIG, + checkApiAvailability, + getTeamStatus, + getDeveloperDetail +}; diff --git a/modules/devboard/components.css b/modules/devboard/components.css new file mode 100644 index 00000000..fd16ad31 --- /dev/null +++ b/modules/devboard/components.css @@ -0,0 +1,156 @@ +/* components.css - 组件样式 */ + +/* 开发者卡片 */ +.dev-card { + background: linear-gradient(145deg, #1e2436, #161b28); + border-radius: 20px; + padding: 20px; + border: 1px solid #2a3040; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + position: relative; + overflow: hidden; +} + +.dev-card:hover { + transform: translateY(-8px) scale(1.02); + border-color: #4a9eff; + box-shadow: 0 20px 30px -10px rgba(74, 158, 255, 0.3); +} + +.dev-card:focus-visible { + outline: 3px solid #ffd700; + outline-offset: 2px; +} + +.dev-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); + transition: left 0.5s; +} + +.dev-card:hover::before { + left: 100%; +} + +.dev-header { + display: flex; + justify-content: space-between; + margin-bottom: 15px; + font-size: 14px; +} + +.dev-id { + color: #4a9eff; + font-weight: bold; + font-family: monospace; +} + +.dev-wins { + background: rgba(255, 215, 0, 0.1); + padding: 4px 8px; + border-radius: 20px; + color: #ffd700; +} + +.dev-name { + font-size: 24px; + font-weight: bold; + margin-bottom: 10px; + color: #fff; +} + +.dev-details { + display: flex; + gap: 10px; + margin-bottom: 15px; + font-size: 14px; +} + +.dev-el { + background: #2a3040; + padding: 4px 8px; + border-radius: 12px; + color: #4a9eff; +} + +.dev-module { + background: #2a3040; + padding: 4px 8px; + border-radius: 12px; + color: #a0a0a0; +} + +.dev-pca { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 5px; + font-size: 11px; +} + +.pca-mini { + text-align: center; + padding: 4px 0; + border-radius: 8px; + background: rgba(0,0,0,0.3); +} + +.pca-mini.EXE { color: #4CAF50; } +.pca-mini.TEC { color: #2196F3; } +.pca-mini.SYS { color: #9C27B0; } +.pca-mini.COL { color: #FF9800; } +.pca-mini.INI { color: #f44336; } + +/* 排行榜项 */ +.ranking-item { + display: flex; + align-items: center; + gap: 15px; + padding: 15px; + border-bottom: 1px solid #2a3040; + transition: background 0.2s; +} + +.ranking-item:hover { + background: #1e2436; +} + +.rank-number { + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + background: #2a3040; + border-radius: 50%; + font-weight: bold; + color: #4a9eff; +} + +.rank-1 .rank-number { background: #ffd700; color: #000; } +.rank-2 .rank-number { background: #c0c0c0; color: #000; } +.rank-3 .rank-number { background: #cd7f32; color: #000; } + +.rank-info { + flex: 1; +} + +.rank-name { + font-weight: bold; + color: #fff; +} + +.rank-id { + font-size: 12px; + color: #888; +} + +.rank-wins { + font-weight: bold; + color: #ffd700; +} diff --git a/modules/devboard/components.js b/modules/devboard/components.js new file mode 100644 index 00000000..5f033e92 --- /dev/null +++ b/modules/devboard/components.js @@ -0,0 +1,146 @@ +// components.js - 组件渲染函数 + +// 模拟数据 +const mockDevelopers = [ + { id: 'DEV-001', name: '小明', pca: { EXE: 85, TEC: 72, SYS: 68, COL: 90, INI: 78 }, wins: 5, el: 'EL-6', module: 'M-DEVBOARD' }, + { id: 'DEV-002', name: '小红', pca: { EXE: 78, TEC: 88, SYS: 82, COL: 75, INI: 92 }, wins: 8, el: 'EL-7', module: 'M-FRONTEND' }, + { id: 'DEV-003', name: '小刚', pca: { EXE: 92, TEC: 65, SYS: 70, COL: 68, INI: 85 }, wins: 3, el: 'EL-5', module: 'M-BACKEND' }, + { id: 'DEV-004', name: '之之', pca: { EXE: 75, TEC: 58, SYS: 82, COL: 82, INI: 85 }, wins: 13, el: 'EL-6', module: 'M-DEVBOARD' } +]; + +// 获取所有开发者 +function getDevelopers() { + return mockDevelopers; +} + +// 根据ID获取开发者 +function getDeveloperById(id) { + return mockDevelopers.find(d => d.id === id) || mockDevelopers[0]; +} + +// 渲染开发者卡片 +function renderDeveloperCards(developers) { + return developers.map(dev => ` +
+
+ ${dev.id} + 🏆 ${dev.wins} +
+
${dev.name}
+
+ ${dev.el} + ${dev.module} +
+
+ ${Object.entries(dev.pca).map(([key, val]) => + `${key}:${val}` + ).join('')} +
+
+ `).join(''); +} + +// 渲染排行榜 +function renderRanking(developers) { + // 按连胜数排序 + const sorted = [...developers].sort((a, b) => b.wins - a.wins); + + return sorted.map((dev, index) => { + const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : ''; + return ` +
+ ${index + 1} +
+
${dev.name}
+
${dev.id}
+
+ ${dev.wins}连胜 +
+ `; + }).join(''); +} + +// 刷新看板 +function refreshDashboard() { + const developers = getDevelopers(); + + // 渲染卡片 + const grid = document.getElementById('developers-grid'); + if (grid) { + grid.innerHTML = renderDeveloperCards(developers); + } + + // 渲染排行榜 + const ranking = document.getElementById('ranking-list'); + if (ranking) { + ranking.innerHTML = renderRanking(developers); + } + + // 绘制团队雷达图(平均分) + const avgPca = { + EXE: Math.round(developers.reduce((sum, d) => sum + d.pca.EXE, 0) / developers.length), + TEC: Math.round(developers.reduce((sum, d) => sum + d.pca.TEC, 0) / developers.length), + SYS: Math.round(developers.reduce((sum, d) => sum + d.pca.SYS, 0) / developers.length), + COL: Math.round(developers.reduce((sum, d) => sum + d.pca.COL, 0) / developers.length), + INI: Math.round(developers.reduce((sum, d) => sum + d.pca.INI, 0) / developers.length) + }; + + drawRadarChart('team-radar', avgPca); +} + +// 搜索筛选 +function filterDevelopers(searchTerm, filterStatus) { + let developers = getDevelopers(); + + // 搜索过滤 + if (searchTerm) { + const term = searchTerm.toLowerCase(); + developers = developers.filter(dev => + dev.name.toLowerCase().includes(term) || + dev.id.toLowerCase().includes(term) || + dev.module.toLowerCase().includes(term) + ); + } + + // 状态过滤(模拟,实际需要真实数据) + if (filterStatus !== 'all') { + // 这里简化处理,实际应该根据模块状态筛选 + developers = developers.filter(dev => { + if (filterStatus === 'active') return dev.wins < 10; + if (filterStatus === 'completed') return dev.wins >= 10; + return true; + }); + } + + return developers; +} + +// 导出函数 +window.getDevelopers = getDevelopers; +window.getDeveloperById = getDeveloperById; +window.renderDeveloperCards = renderDeveloperCards; +window.renderRanking = renderRanking; +window.refreshDashboard = refreshDashboard; +window.filterDevelopers = filterDevelopers; + +// 增强版排行榜渲染(带数字滚动支持) +function renderRankingWithAnimation(developers) { + const sorted = [...developers].sort((a, b) => b.wins - a.wins); + + return sorted.map((dev, index) => { + const rankClass = index === 0 ? 'rank-1' : index === 1 ? 'rank-2' : index === 2 ? 'rank-3' : ''; + return ` +
+ ${index + 1} +
+
${dev.name}
+
${dev.id}
+
+ ${dev.wins}连胜 +
+ `; + }).join(''); +} + +// 覆盖原函数 +window.renderRanking = renderRankingWithAnimation; diff --git a/modules/devboard/detail.css b/modules/devboard/detail.css new file mode 100644 index 00000000..385c4846 --- /dev/null +++ b/modules/devboard/detail.css @@ -0,0 +1,358 @@ +/* detail.css - 详情页专用样式 */ + +/* 详情页容器 */ +#detail-container { + display: none; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + min-height: 100vh; + padding: 20px; + animation: fadeIn 0.3s ease; +} + +/* 详情页头部 */ +.detail-header { + margin-bottom: 30px; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +.back-button { + background: rgba(255, 255, 255, 0.1); + border: 2px solid #4a9eff; + color: #fff; + padding: 12px 24px; + border-radius: 30px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(5px); +} + +.back-button:hover { + background: #4a9eff; + transform: translateX(-5px); + box-shadow: 0 0 20px rgba(74, 158, 255, 0.5); +} + +.back-button:focus-visible { + outline: 3px solid #ffd700; + outline-offset: 2px; +} + +/* 详情页内容区 */ +.detail-content { + display: flex; + flex-direction: column; + gap: 30px; + max-width: 1200px; + margin: 0 auto; +} + +.detail-section { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 25px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.detail-section h2 { + color: #4a9eff; + font-size: 20px; + margin-bottom: 20px; + border-bottom: 2px solid rgba(74, 158, 255, 0.3); + padding-bottom: 10px; + display: flex; + align-items: center; + gap: 10px; +} + +/* 开发者大头卡片(大尺寸) */ +.developer-card-large .dev-card { + transform: scale(1); + background: linear-gradient(145deg, #1e2436, #161b28); + border: 2px solid #4a9eff; + margin-bottom: 0; +} + +.developer-card-large .dev-card:hover { + transform: translateY(-5px); + box-shadow: 0 20px 30px -10px rgba(74, 158, 255, 0.5); +} + +/* PCA雷达图容器 */ +.radar-large-container { + height: 300px; + margin-bottom: 20px; + background: rgba(0, 0, 0, 0.2); + border-radius: 12px; + padding: 10px; +} + +.radar-large-container canvas { + width: 100%; + height: 100%; +} + +/* PCA分数标签 */ +.pca-scores { + display: flex; + flex-wrap: wrap; + gap: 15px; + justify-content: center; + margin-top: 20px; +} + +.score-tag { + padding: 8px 16px; + border-radius: 20px; + font-weight: bold; + font-size: 14px; + background: rgba(0, 0, 0, 0.5); + border: 1px solid; + transition: transform 0.2s; +} + +.score-tag:hover { + transform: scale(1.05); +} + +.score-tag.EXE { border-color: #4CAF50; color: #4CAF50; } +.score-tag.TEC { border-color: #2196F3; color: #2196F3; } +.score-tag.SYS { border-color: #9C27B0; color: #9C27B0; } +.score-tag.COL { border-color: #FF9800; color: #FF9800; } +.score-tag.INI { border-color: #f44336; color: #f44336; } + +/* 五维进度条 */ +.progress-bars-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.progress-item { + display: flex; + align-items: center; + gap: 15px; + padding: 10px; + background: rgba(0, 0, 0, 0.2); + border-radius: 12px; + transition: transform 0.2s ease, background 0.2s; +} + +.progress-item:hover { + transform: translateX(10px); + background: rgba(0, 0, 0, 0.3); +} + +.progress-label { + width: 100px; + display: flex; + justify-content: space-between; + font-weight: bold; +} + +.dim-name { + color: #fff; +} + +.dim-score { + color: #ffd700; + font-family: monospace; +} + +.progress-bar-bg { + flex: 1; + height: 24px; + background: rgba(255, 255, 255, 0.1); + border-radius: 12px; + overflow: hidden; + position: relative; +} + +.progress-bar-fill { + height: 100%; + border-radius: 12px; + transition: width 1s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.progress-bar-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: shimmer 2s infinite; +} + +.progress-dim-code { + width: 50px; + text-align: right; + font-family: monospace; + color: #888; + font-size: 14px; +} + +/* 模块历史列表 */ +.module-history-list { + list-style: none; + padding: 0; + margin: 0; +} + +.module-item { + display: flex; + align-items: center; + gap: 20px; + padding: 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + transition: background 0.2s ease; +} + +.module-item:hover { + background: rgba(255, 255, 255, 0.05); + transform: scale(1.01); +} + +.module-code { + font-family: monospace; + color: #4a9eff; + font-weight: bold; + width: 150px; +} + +.module-name { + flex: 1; + color: #fff; +} + +.module-status { + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: bold; + width: 80px; + text-align: center; +} + +.status-已完成 { background: rgba(76, 175, 80, 0.2); color: #4CAF50; border: 1px solid #4CAF50; } +.status-进行中 { background: rgba(255, 152, 0, 0.2); color: #FF9800; border: 1px solid #FF9800; } +.status-待开始 { background: rgba(158, 158, 158, 0.2); color: #9E9E9E; border: 1px solid #9E9E9E; } + +.module-date { + color: #888; + font-size: 14px; + width: 100px; + font-family: monospace; +} + +/* 趋势图容器 */ +.trend-chart-container { + background: rgba(0, 0, 0, 0.3); + border-radius: 12px; + padding: 20px; + position: relative; +} + +/* 无数据提示 */ +.no-data { + text-align: center; + color: #888; + padding: 40px; + font-style: italic; +} + +/* 动画定义 */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadeOut { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(20px); } +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +.fade-in { + animation: fadeIn 0.3s ease forwards; +} + +.fade-out { + animation: fadeOut 0.3s ease forwards; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .detail-content { + gap: 15px; + } + + .detail-section { + padding: 15px; + } + + .progress-item { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .progress-label { + width: 100%; + } + + .progress-bar-bg { + width: 100%; + } + + .progress-dim-code { + width: 100%; + text-align: left; + } + + .module-item { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .module-code { + width: auto; + } + + .module-name { + width: 100%; + } + + .module-status { + width: auto; + } + + .module-date { + width: auto; + } + + .pca-scores { + gap: 8px; + } + + .score-tag { + padding: 4px 12px; + font-size: 12px; + } + + .radar-large-container { + height: 250px; + } +} diff --git a/modules/devboard/detail.js b/modules/devboard/detail.js new file mode 100644 index 00000000..7996cbb6 --- /dev/null +++ b/modules/devboard/detail.js @@ -0,0 +1,354 @@ +// detail.js - 开发者详情页渲染器 + +let currentDevId = null; + +// 显示详情页 +function showDetailPage(devId) { + currentDevId = devId; + + const overview = document.getElementById('overview-container'); + const detail = document.getElementById('detail-container'); + + if (!overview || !detail) { + console.error('找不到容器元素'); + return; + } + + // 淡出总览页 + overview.classList.add('fade-out'); + + setTimeout(() => { + overview.style.display = 'none'; + overview.classList.remove('fade-out'); + + // 渲染详情页数据 + renderDetailPage(devId); + + detail.style.display = 'block'; + detail.classList.add('fade-in'); + + // 滚动到顶部 + window.scrollTo(0, 0); + }, 300); +} + +// 返回总览页 +function hideDetailPage() { + const overview = document.getElementById('overview-container'); + const detail = document.getElementById('detail-container'); + + detail.classList.add('fade-out'); + + setTimeout(() => { + detail.style.display = 'none'; + detail.classList.remove('fade-out'); + + overview.style.display = 'block'; + overview.classList.add('fade-in'); + + // 刷新总览页数据 + refreshDashboard(); + }, 300); +} + +// 渲染详情页 +function renderDetailPage(devId) { + const dev = getDeveloperById(devId); + if (!dev) return; + + const container = document.getElementById('detail-container'); + + // 生成五维进度条 + const progressBars = renderPCAProgressBars(dev.pca); + + // 获取模块历史 + const modules = getModuleHistory(devId); + const moduleList = renderModuleHistory(modules); + + // 获取连胜趋势数据 + const trendData = getWinTrendData(devId); + + container.innerHTML = ` +
+ +
+
+ +
+
+
+ ${dev.id} + 🏆 ${dev.wins} +
+
${dev.name}
+
+ ${dev.el} + ${dev.module} +
+
+ ${Object.entries(dev.pca).map(([key, val]) => + `${key}:${val}` + ).join('')} +
+
+
+ + +
+

📊 完整PCA雷达图

+
+ ${renderRadarChart(dev.pca, 'large')} +
+
+ ${Object.entries(dev.pca).map(([dim, score]) => + `${dim}: ${score}` + ).join('')} +
+
+ + +
+

📈 五维进度条

+
+ ${progressBars} +
+
+ + +
+

📋 模块历史

+
+ ${moduleList} +
+
+ + +
+

🏆 连胜趋势

+
+ + +
+
+
+ `; + + // 绘制趋势图 + setTimeout(() => drawTrendChart('trend-canvas', trendData), 100); +} + +// 渲染PCA五维进度条 +function renderPCAProgressBars(pca) { + const dimensions = [ + { key: 'EXE', label: '执行力', color: '#4CAF50' }, + { key: 'TEC', label: '技术深度', color: '#2196F3' }, + { key: 'SYS', label: '系统思维', color: '#9C27B0' }, + { key: 'COL', label: '协作力', color: '#FF9800' }, + { key: 'INI', label: '主动性', color: '#f44336' } + ]; + + return dimensions.map(dim => { + const score = pca[dim.key] || 0; + + return ` +
+
+ ${dim.label} + ${score} +
+
+
+
+
+
${dim.key}
+
+ `; + }).join(''); +} + +// 渲染模块历史列表 +function renderModuleHistory(modules) { + if (!modules || modules.length === 0) { + return '

暂无模块历史

'; + } + + return ` + + `; +} + +// 获取模块历史(模拟数据) +function getModuleHistory(devId) { + const histories = { + 'DEV-001': [ + { code: 'M-DEVBOARD-001', name: '开发者看板·环节0~2', status: '已完成', completedDate: '2026-03-10' }, + { code: 'M-FRONTEND-003', name: '前端基础组件', status: '已完成', completedDate: '2026-03-08' }, + { code: 'M-API-002', name: 'API对接练习', status: '进行中', completedDate: null } + ], + 'DEV-002': [ + { code: 'M-FRONTEND-001', name: '前端架构设计', status: '已完成', completedDate: '2026-03-09' }, + { code: 'M-UI-005', name: '组件库开发', status: '已完成', completedDate: '2026-03-07' }, + { code: 'M-TEST-003', name: '单元测试编写', status: '待开始', completedDate: null } + ], + 'DEV-003': [ + { code: 'M-BACKEND-002', name: 'API服务搭建', status: '已完成', completedDate: '2026-03-11' }, + { code: 'M-DB-001', name: '数据库设计', status: '已完成', completedDate: '2026-03-09' }, + { code: 'M-DEPLOY-002', name: '容器化部署', status: '进行中', completedDate: null } + ], + 'DEV-004': [ + { code: 'M-DEVBOARD-001', name: '开发者看板·环节0~2', status: '已完成', completedDate: '2026-03-11' }, + { code: 'M-CANVAS-005', name: 'Canvas雷达图绘制', status: '已完成', completedDate: '2026-03-09' }, + { code: 'M-COMPONENTS-003', name: '组件化思维训练', status: '已完成', completedDate: '2026-03-07' }, + { code: 'M-DEPLOY-001', name: '静态部署练习', status: '待开始', completedDate: null } + ] + }; + return histories[devId] || histories['DEV-001']; +} + +// 获取连胜趋势数据 +function getWinTrendData(devId) { + const trends = { + 'DEV-001': [ + { date: '03-06', value: 3 }, + { date: '03-07', value: 3 }, + { date: '03-08', value: 4 }, + { date: '03-09', value: 4 }, + { date: '03-10', value: 5 }, + { date: '03-11', value: 5 }, + { date: '03-12', value: 5 } + ], + 'DEV-002': [ + { date: '03-06', value: 5 }, + { date: '03-07', value: 6 }, + { date: '03-08', value: 6 }, + { date: '03-09', value: 7 }, + { date: '03-10', value: 7 }, + { date: '03-11', value: 8 }, + { date: '03-12', value: 8 } + ], + 'DEV-003': [ + { date: '03-06', value: 1 }, + { date: '03-07', value: 1 }, + { date: '03-08', value: 2 }, + { date: '03-09', value: 2 }, + { date: '03-10', value: 3 }, + { date: '03-11', value: 3 }, + { date: '03-12', value: 3 } + ], + 'DEV-004': [ + { date: '03-06', value: 8 }, + { date: '03-07', value: 9 }, + { date: '03-08', value: 9 }, + { date: '03-09', value: 10 }, + { date: '03-10', value: 11 }, + { date: '03-11', value: 12 }, + { date: '03-12', value: 13 } + ] + }; + return trends[devId] || trends['DEV-001']; +} + +// 绘制趋势图 +function drawTrendChart(canvasId, data) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + const padding = 40; + + ctx.clearRect(0, 0, width, height); + + if (!data || data.length < 2) { + ctx.fillStyle = '#888'; + ctx.font = '14px monospace'; + ctx.fillText('暂无连胜数据', padding, height/2); + return; + } + + // 计算坐标范围 + const values = data.map(d => d.value); + const maxValue = Math.max(...values) + 1; + const minValue = Math.max(0, Math.min(...values) - 1); + const xStep = (width - 2 * padding) / (data.length - 1); + + // 绘制网格线 + ctx.strokeStyle = '#333'; + ctx.lineWidth = 1; + ctx.beginPath(); + for (let i = 0; i <= 5; i++) { + const y = padding + (i * (height - 2 * padding) / 5); + ctx.moveTo(padding, y); + ctx.lineTo(width - padding, y); + } + ctx.strokeStyle = '#444'; + ctx.stroke(); + + // 绘制轴线 + ctx.beginPath(); + ctx.strokeStyle = '#666'; + ctx.lineWidth = 2; + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // 绘制折线 + ctx.beginPath(); + ctx.strokeStyle = '#4CAF50'; + ctx.lineWidth = 3; + + data.forEach((point, i) => { + const x = padding + i * xStep; + const y = padding + (height - 2 * padding) * (1 - (point.value - minValue) / (maxValue - minValue)); + + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + }); + ctx.stroke(); + + // 绘制数据点 + data.forEach((point, i) => { + const x = padding + i * xStep; + const y = padding + (height - 2 * padding) * (1 - (point.value - minValue) / (maxValue - minValue)); + + ctx.beginPath(); + ctx.arc(x, y, 6, 0, Math.PI * 2); + ctx.fillStyle = '#FFD700'; + ctx.fill(); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.stroke(); + + // 显示数值 + ctx.fillStyle = '#fff'; + ctx.font = 'bold 12px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(point.value, x, y - 15); + + // 显示日期 + ctx.fillStyle = '#aaa'; + ctx.font = '10px monospace'; + ctx.fillText(point.date, x, height - padding + 20); + }); +} + +// 导出函数 +window.showDetailPage = showDetailPage; +window.hideDetailPage = hideDetailPage; +window.getModuleHistory = getModuleHistory; +window.getWinTrendData = getWinTrendData; diff --git a/modules/devboard/index.html b/modules/devboard/index.html index e9deb195..f671be0a 100644 --- a/modules/devboard/index.html +++ b/modules/devboard/index.html @@ -1,49 +1,53 @@ - + - - HoloLake 开发者实时看板 - - - + + 开发者实时看板 · 环节0~4 · 之之 + + + - - -
- + -
-
-
-
+ +
- -
+ +
+
+

团队PCA雷达

+ +
+
+

连胜排行榜

+
+
+
+ - - - - - - + + + + + + + diff --git a/modules/devboard/main.js b/modules/devboard/main.js new file mode 100644 index 00000000..3ace8620 --- /dev/null +++ b/modules/devboard/main.js @@ -0,0 +1,236 @@ +// main.js - 主逻辑(增强版) + +// 页面加载完成后初始化 +document.addEventListener('DOMContentLoaded', function() { + // 初始渲染 + refreshDashboard(); + + // 搜索功能(带防抖和动画) + const searchInput = document.getElementById('search-input'); + if (searchInput) { + let searchTimer; + searchInput.addEventListener('input', function(e) { + clearTimeout(searchTimer); + + // 添加输入时的微动效 + this.style.transform = 'scale(1.02)'; + setTimeout(() => { + this.style.transform = ''; + }, 150); + + searchTimer = setTimeout(() => { + applyFilters(); + }, 300); + }); + } + + // 筛选按钮 + const filterBtns = document.querySelectorAll('.filter-btn'); + filterBtns.forEach(btn => { + btn.addEventListener('click', function() { + // 按钮点击动画 + this.style.transform = 'scale(0.95)'; + setTimeout(() => { + this.style.transform = ''; + }, 150); + + // 更新按钮状态 + filterBtns.forEach(b => b.classList.remove('active')); + this.classList.add('active'); + + // 应用筛选 + applyFilters(); + }); + }); + + // 卡片点击事件(使用事件委托) + document.addEventListener('click', function(e) { + const card = e.target.closest('.dev-card'); + if (card) { + const devId = card.dataset.devId; + if (devId && window.showDetailPage) { + // 添加点击动画 + card.style.transform = 'scale(0.98)'; + card.style.opacity = '0.8'; + + setTimeout(() => { + card.style.transform = ''; + card.style.opacity = ''; + window.showDetailPage(devId); + }, 150); + } + } + }); + + // 键盘导航 + document.addEventListener('keydown', function(e) { + // 获取所有可聚焦的卡片 + const cards = Array.from(document.querySelectorAll('.dev-card')); + const currentIndex = cards.indexOf(document.activeElement); + + // Tab键(默认行为已支持,我们只加样式) + if (e.key === 'Tab') { + setTimeout(() => { + const focused = document.activeElement; + if (focused.classList.contains('dev-card')) { + focused.style.outline = '3px solid #ffd700'; + focused.style.transform = 'scale(1.02)'; + } + }, 50); + } + + // 箭头键导航 + if (e.key === 'ArrowRight' || e.key === 'ArrowLeft' || + e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault(); + + if (cards.length === 0) return; + + const cols = Math.floor(document.querySelector('.developers-grid').offsetWidth / 300); + let nextIndex; + + if (e.key === 'ArrowRight') { + nextIndex = currentIndex + 1; + } else if (e.key === 'ArrowLeft') { + nextIndex = currentIndex - 1; + } else if (e.key === 'ArrowDown') { + nextIndex = currentIndex + cols; + } else if (e.key === 'ArrowUp') { + nextIndex = currentIndex - cols; + } + + if (nextIndex >= 0 && nextIndex < cards.length) { + cards[nextIndex].focus(); + } + } + + // Enter/Space 打开详情 + if (e.key === 'Enter' || e.key === ' ') { + const target = e.target.closest('.dev-card'); + if (target) { + e.preventDefault(); + const devId = target.dataset.devId; + if (devId && window.showDetailPage) { + window.showDetailPage(devId); + } + } + } + + // ESC返回总览 + if (e.key === 'Escape') { + const detail = document.getElementById('detail-container'); + if (detail && detail.style.display !== 'none' && window.hideDetailPage) { + window.hideDetailPage(); + } + } + }); + + // 失去焦点时移除高亮 + document.addEventListener('blur', function(e) { + if (e.target.classList.contains('dev-card')) { + e.target.style.outline = ''; + e.target.style.transform = ''; + } + }, true); +}); + +// 应用搜索和筛选 +function applyFilters() { + const searchTerm = document.getElementById('search-input')?.value || ''; + const activeFilter = document.querySelector('.filter-btn.active')?.dataset.filter || 'all'; + + // 获取筛选后的开发者 + const filtered = filterDevelopers(searchTerm, activeFilter); + + // 重新渲染卡片 + const grid = document.getElementById('developers-grid'); + if (grid) { + // 添加淡出动画 + grid.style.opacity = '0.5'; + + setTimeout(() => { + if (filtered.length === 0) { + // 显示无结果提示 + grid.innerHTML = '
🔍 没有找到匹配的开发者
'; + } else { + grid.innerHTML = renderDeveloperCards(filtered); + + // 添加展开/收起动画 + Array.from(grid.children).forEach((card, index) => { + card.style.animation = `cardPopIn 0.3s ease ${index * 0.05}s forwards`; + card.style.opacity = '0'; + setTimeout(() => { + card.style.opacity = '1'; + }, 50 + index * 20); + }); + } + + grid.style.opacity = '1'; + }, 200); + } + + // 更新排行榜(排行榜不筛选,但高亮匹配项) + const ranking = document.getElementById('ranking-list'); + if (ranking) { + const allDevs = getDevelopers(); + ranking.innerHTML = renderRanking(allDevs); + + // 高亮筛选出的开发者 + if (filtered.length < allDevs.length && filtered.length > 0) { + const filteredIds = filtered.map(d => d.id); + document.querySelectorAll('.ranking-item').forEach(item => { + const nameEl = item.querySelector('.rank-name'); + if (nameEl) { + const dev = allDevs.find(d => d.name === nameEl.textContent); + if (dev && filteredIds.includes(dev.id)) { + item.classList.add('match-highlight'); + + // 添加数字滚动动画 + const winsEl = item.querySelector('.rank-wins'); + if (winsEl) { + const oldValue = parseInt(winsEl.textContent) || 0; + animateNumber(winsEl, oldValue, dev.wins, 500); + } + } else { + item.classList.remove('match-highlight'); + } + } + }); + } else { + document.querySelectorAll('.ranking-item').forEach(item => { + item.classList.remove('match-highlight'); + }); + } + } +} + +// 数字滚动动画 +function animateNumber(element, start, end, duration = 1000) { + const range = end - start; + const increment = range / (duration / 16); + let current = start; + const startTime = performance.now(); + + function update(currentTime) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + + // 缓动函数 + const easeOutQuart = 1 - Math.pow(1 - progress, 3); + current = start + (range * easeOutQuart); + + element.textContent = Math.round(current) + (element.textContent.includes('连胜') ? '连胜' : ''); + + if (progress < 1) { + requestAnimationFrame(update); + } else { + element.textContent = end + (element.textContent.includes('连胜') ? '连胜' : ''); + } + } + + requestAnimationFrame(update); +} + +// 导出函数 +window.applyFilters = applyFilters; +window.animateNumber = animateNumber; diff --git a/modules/devboard/radar.js b/modules/devboard/radar.js new file mode 100644 index 00000000..2cd2d867 --- /dev/null +++ b/modules/devboard/radar.js @@ -0,0 +1,116 @@ +// radar.js - 雷达图绘制 +function drawRadarChart(canvasId, data) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + const centerX = width / 2; + const centerY = height / 2; + const radius = Math.min(width, height) * 0.35; + + const dimensions = ['EXE', 'TEC', 'SYS', 'COL', 'INI']; + const angleStep = (Math.PI * 2) / dimensions.length; + + // 清空画布 + ctx.clearRect(0, 0, width, height); + + // 绘制背景网格 + ctx.strokeStyle = '#2a3040'; + ctx.lineWidth = 1; + + // 绘制5层同心圆 + for (let level = 1; level <= 5; level++) { + ctx.beginPath(); + for (let i = 0; i <= dimensions.length; i++) { + const angle = i * angleStep - Math.PI / 2; + const x = centerX + radius * (level / 5) * Math.cos(angle); + const y = centerY + radius * (level / 5) * Math.sin(angle); + + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.closePath(); + ctx.strokeStyle = '#2a3040'; + ctx.stroke(); + } + + // 绘制轴线 + for (let i = 0; i < dimensions.length; i++) { + const angle = i * angleStep - Math.PI / 2; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.lineTo(x, y); + ctx.strokeStyle = '#3a4050'; + ctx.stroke(); + + // 标注维度 + const labelX = centerX + (radius + 15) * Math.cos(angle); + const labelY = centerY + (radius + 15) * Math.sin(angle); + ctx.fillStyle = '#4a9eff'; + ctx.font = 'bold 14px monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(dimensions[i], labelX, labelY); + } + + // 绘制数据 + if (data) { + ctx.beginPath(); + for (let i = 0; i < dimensions.length; i++) { + const value = data[dimensions[i]] || 0; + const angle = i * angleStep - Math.PI / 2; + const r = radius * (value / 100); + const x = centerX + r * Math.cos(angle); + const y = centerY + r * Math.sin(angle); + + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.closePath(); + ctx.fillStyle = 'rgba(74, 158, 255, 0.3)'; + ctx.fill(); + ctx.strokeStyle = '#4a9eff'; + ctx.lineWidth = 3; + ctx.stroke(); + + // 绘制数据点 + for (let i = 0; i < dimensions.length; i++) { + const value = data[dimensions[i]] || 0; + const angle = i * angleStep - Math.PI / 2; + const r = radius * (value / 100); + const x = centerX + r * Math.cos(angle); + const y = centerY + r * Math.sin(angle); + + ctx.beginPath(); + ctx.arc(x, y, 5, 0, Math.PI * 2); + ctx.fillStyle = '#fff'; + ctx.fill(); + ctx.strokeStyle = '#4a9eff'; + ctx.lineWidth = 2; + ctx.stroke(); + } + } +} + +// 渲染雷达图HTML(供detail.js调用) +function renderRadarChart(pcaData, size = 'normal') { + const canvasId = 'radar-' + Math.random().toString(36).substr(2, 9); + const width = size === 'large' ? 400 : 300; + const height = size === 'large' ? 300 : 250; + + setTimeout(() => { + const canvas = document.getElementById(canvasId); + if (canvas) drawRadarChart(canvas.id, pcaData); + }, 100); + + return ``; +} + +// 导出函数 +window.drawRadarChart = drawRadarChart; +window.renderRadarChart = renderRadarChart; diff --git a/modules/devboard/style.css b/modules/devboard/style.css new file mode 100644 index 00000000..18ed43c5 --- /dev/null +++ b/modules/devboard/style.css @@ -0,0 +1,287 @@ +/* style.css - 基础样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; + background: #0a0c14; + color: #e0e0e0; + line-height: 1.6; +} + +/* 总览页容器 */ +#overview-container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +/* 头部 */ +.dashboard-header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #1a1f2e, #0f1219); + border-radius: 20px; + border: 1px solid #2a3040; +} + +.dashboard-header h1 { + color: #4a9eff; + font-size: 2em; + margin-bottom: 10px; + text-shadow: 0 0 20px rgba(74, 158, 255, 0.3); +} + +/* 控制区 */ +.controls-section { + display: flex; + gap: 20px; + margin-bottom: 30px; + flex-wrap: wrap; +} + +.search-box { + flex: 1; + min-width: 250px; + padding: 12px 20px; + background: #1a1f2e; + border: 2px solid #2a3040; + border-radius: 30px; + color: #fff; + font-size: 16px; + transition: all 0.3s; +} + +.search-box:focus { + outline: none; + border-color: #4a9eff; + box-shadow: 0 0 20px rgba(74, 158, 255, 0.2); +} + +.filter-buttons { + display: flex; + gap: 10px; +} + +.filter-btn { + padding: 10px 24px; + background: #1a1f2e; + border: 2px solid #2a3040; + border-radius: 30px; + color: #a0a0a0; + font-weight: bold; + cursor: pointer; + transition: all 0.3s; +} + +.filter-btn.active { + background: #4a9eff; + border-color: #4a9eff; + color: #fff; +} + +.filter-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(74, 158, 255, 0.2); +} + +/* 开发者网格 */ +.developers-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +/* 统计区 */ +.stats-section { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.radar-container, .ranking-container { + background: #1a1f2e; + border-radius: 20px; + padding: 20px; + border: 1px solid #2a3040; +} + +.radar-container h2, .ranking-container h2 { + color: #4a9eff; + margin-bottom: 20px; + font-size: 1.2em; +} + +#team-radar { + width: 100%; + height: auto; + max-height: 300px; +} + +/* 响应式 */ +@media (max-width: 768px) { + .stats-section { + grid-template-columns: 1fr; + } + + .controls-section { + flex-direction: column; + } + + .filter-buttons { + justify-content: center; + } +} + +/* 动画 */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in { + animation: fadeIn 0.5s ease forwards; +} + +/* ===== 交互增强样式 ===== */ + +/* 搜索框增强 */ +.search-box { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.search-box:focus { + transform: scale(1.02); + background: #1e2436; +} + +/* 筛选按钮动画 */ +.filter-btn { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.filter-btn::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: translate(-50%, -50%); + transition: width 0.3s, height 0.3s; +} + +.filter-btn:active::after { + width: 100px; + height: 100px; +} + +/* 卡片网格动画 */ +.developers-grid { + transition: opacity 0.3s ease; + min-height: 400px; +} + +/* 卡片展开/收起动画 */ +.dev-card { + animation: cardPopIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transform-origin: center; +} + +@keyframes cardPopIn { + 0% { + opacity: 0; + transform: scale(0.8) translateY(20px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* 键盘焦点样式 */ +.dev-card:focus-visible { + outline: 3px solid #ffd700; + outline-offset: 2px; + transform: scale(1.02); + box-shadow: 0 0 30px rgba(255, 215, 0, 0.3); +} + +/* 数字滚动动画 */ +.stat-value { + display: inline-block; + transition: all 0.3s ease; + font-feature-settings: "tnum"; +} + +/* 无结果提示 */ +.no-results { + grid-column: 1 / -1; + text-align: center; + padding: 60px; + background: rgba(255, 255, 255, 0.05); + border-radius: 20px; + color: #888; + font-size: 18px; + border: 2px dashed #2a3040; + animation: fadeIn 0.5s ease; +} + +/* 高亮筛选匹配项 */ +.ranking-item.match-highlight { + background: rgba(74, 158, 255, 0.2); + border-left: 4px solid #4a9eff; + transform: translateX(5px); +} + +/* 淡入淡出动画增强 */ +.fade-in { + animation: fadeIn 0.3s ease forwards; +} + +.fade-out { + animation: fadeOut 0.3s ease forwards; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-10px); + } +} + +/* 响应式优化 */ +@media (max-width: 768px) { + .dev-card:focus-visible { + transform: scale(1.01); + } + + .filter-btn:active::after { + width: 60px; + height: 60px; + } +}