DEV-004: M-DEVBOARD环节3-4-详情页+部署+API对接·看板从本地到线上·十四连胜
This commit is contained in:
parent
b7236d0236
commit
54bc99acb9
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue