DEV-004: M-DEVBOARD环节3-4-详情页+部署+API对接·看板从本地到线上·十四连胜

This commit is contained in:
zhizhi200271 2026-03-12 17:18:25 +08:00
parent b7236d0236
commit 54bc99acb9
9 changed files with 1774 additions and 38 deletions

79
modules/devboard/api.js Normal file
View File

@ -0,0 +1,79 @@
// api.js - API配置和模拟数据切换
const CONFIG = {
// API基础地址页页的接口
BASE_URL: 'https://guanghulab.com/api',
// 是否使用真实APIfalse = 使用模拟数据)
USE_REAL_API: false,
// API端点
endpoints: {
teamStatus: '/dashboard/team-status',
developerDetail: '/dashboard/dev/', // + id
modules: '/dashboard/modules'
}
};
// 检查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
};

View File

@ -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;
}

View File

@ -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 => `
<div class="dev-card" data-dev-id="${dev.id}" tabindex="0" role="button" aria-label="查看${dev.name}的详情">
<div class="dev-header">
<span class="dev-id">${dev.id}</span>
<span class="dev-wins">🏆 ${dev.wins}</span>
</div>
<div class="dev-name">${dev.name}</div>
<div class="dev-details">
<span class="dev-el">${dev.el}</span>
<span class="dev-module">${dev.module}</span>
</div>
<div class="dev-pca">
${Object.entries(dev.pca).map(([key, val]) =>
`<span class="pca-mini ${key}">${key}:${val}</span>`
).join('')}
</div>
</div>
`).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 `
<div class="ranking-item ${rankClass}">
<span class="rank-number">${index + 1}</span>
<div class="rank-info">
<div class="rank-name">${dev.name}</div>
<div class="rank-id">${dev.id}</div>
</div>
<span class="rank-wins">${dev.wins}连胜</span>
</div>
`;
}).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 `
<div class="ranking-item ${rankClass}" data-dev-id="${dev.id}">
<span class="rank-number">${index + 1}</span>
<div class="rank-info">
<div class="rank-name">${dev.name}</div>
<div class="rank-id">${dev.id}</div>
</div>
<span class="rank-wins stat-value">${dev.wins}连胜</span>
</div>
`;
}).join('');
}
// 覆盖原函数
window.renderRanking = renderRankingWithAnimation;

358
modules/devboard/detail.css Normal file
View File

@ -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;
}
}

354
modules/devboard/detail.js Normal file
View File

@ -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 = `
<div class="detail-header">
<button class="back-button" onclick="hideDetailPage()" aria-label="返回总览">
返回总览
</button>
</div>
<div class="detail-content">
<!-- 开发者大头卡片 -->
<div class="detail-section developer-card-large">
<div class="dev-card" data-dev-id="${dev.id}">
<div class="dev-header">
<span class="dev-id">${dev.id}</span>
<span class="dev-wins">🏆 ${dev.wins}</span>
</div>
<div class="dev-name">${dev.name}</div>
<div class="dev-details">
<span class="dev-el">${dev.el}</span>
<span class="dev-module">${dev.module}</span>
</div>
<div class="dev-pca">
${Object.entries(dev.pca).map(([key, val]) =>
`<span class="pca-mini ${key}">${key}:${val}</span>`
).join('')}
</div>
</div>
</div>
<!-- PCA雷达图 -->
<div class="detail-section">
<h2>📊 完整PCA雷达图</h2>
<div class="radar-large-container">
${renderRadarChart(dev.pca, 'large')}
</div>
<div class="pca-scores">
${Object.entries(dev.pca).map(([dim, score]) =>
`<span class="score-tag ${dim}">${dim}: ${score}</span>`
).join('')}
</div>
</div>
<!-- 五维进度条 -->
<div class="detail-section">
<h2>📈 五维进度条</h2>
<div class="progress-bars-container">
${progressBars}
</div>
</div>
<!-- 模块历史 -->
<div class="detail-section">
<h2>📋 模块历史</h2>
<div class="module-history-container">
${moduleList}
</div>
</div>
<!-- 连胜趋势 -->
<div class="detail-section">
<h2>🏆 连胜趋势</h2>
<div class="trend-chart-container">
<canvas id="trend-canvas" width="400" height="200"
style="width:100%; height:200px; background: rgba(0,0,0,0.2); border-radius: 8px;">
</canvas>
</div>
</div>
</div>
`;
// 绘制趋势图
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 `
<div class="progress-item">
<div class="progress-label">
<span class="dim-name">${dim.label}</span>
<span class="dim-score">${score}</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill"
style="width: ${score}%; background-color: ${dim.color};">
</div>
</div>
<div class="progress-dim-code">${dim.key}</div>
</div>
`;
}).join('');
}
// 渲染模块历史列表
function renderModuleHistory(modules) {
if (!modules || modules.length === 0) {
return '<p class="no-data">暂无模块历史</p>';
}
return `
<ul class="module-history-list">
${modules.map(m => `
<li class="module-item status-${m.status}">
<span class="module-code">${m.code}</span>
<span class="module-name">${m.name}</span>
<span class="module-status">${m.status}</span>
<span class="module-date">${m.completedDate || m.startDate || '-'}</span>
</li>
`).join('')}
</ul>
`;
}
// 获取模块历史(模拟数据)
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;

View File

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

236
modules/devboard/main.js Normal file
View File

@ -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 = '<div class="no-results">🔍 没有找到匹配的开发者</div>';
} 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;

116
modules/devboard/radar.js Normal file
View File

@ -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 `<canvas id="${canvasId}" width="${width}" height="${height}" style="width:100%; height:auto;"></canvas>`;
}
// 导出函数
window.drawRadarChart = drawRadarChart;
window.renderRadarChart = renderRadarChart;

287
modules/devboard/style.css Normal file
View File

@ -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;
}
}