You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openrank/front/frontend/app.js

1114 lines
41 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// OpenRank 前端应用 - 主逻辑文件
class OpenRankDashboard {
constructor() {
this.currentTab = 'overview';
this.charts = {};
this.mockData = this.generateMockData();
this.init();
}
// 初始化应用
init() {
this.setupEventListeners();
this.loadMockData();
this.initCharts();
this.updateLastUpdateTime();
this.setupTabNavigation();
// 默认显示开发者分析数据
this.displayDeveloperAnalysis(this.mockData.developers);
}
// 设置事件监听器
setupEventListeners() {
// 计算按钮点击事件
document.getElementById('calculate-btn').addEventListener('click', () => {
this.calculateOpenRank();
});
// 搜索功能
const searchInput = document.getElementById('developer-search');
searchInput.addEventListener('input', (e) => {
this.filterDevelopers(e.target.value);
});
// 仓库过滤器
document.getElementById('repo-filter').addEventListener('change', (e) => {
this.filterRepositories(e.target.value);
});
// 配置表单变化
this.setupConfigListeners();
}
// 设置标签页导航
setupTabNavigation() {
const navButtons = document.querySelectorAll('.nav-btn');
navButtons.forEach(button => {
button.addEventListener('click', (e) => {
const tab = e.target.dataset.tab;
this.switchTab(tab);
});
});
}
// 切换标签页
switchTab(tab) {
// 移除所有活动状态
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// 添加新的活动状态
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
document.getElementById(tab).classList.add('active');
this.currentTab = tab;
// 如果是网络图谱标签,初始化图谱
if (tab === 'network') {
this.initNetworkGraph();
}
}
// 设置配置监听器
setupConfigListeners() {
const configInputs = document.querySelectorAll('.setting-item input');
configInputs.forEach(input => {
input.addEventListener('change', (e) => {
this.updateConfig(e.target.id, e.target.value);
});
});
}
// 生成模拟数据(包含丰富的示例数据)
generateMockData() {
// 创建有意义的开发者示例数据
const exampleDevelopers = [
{
id: 1,
name: "张三",
openrank: 95.2,
change: 12.5,
activity: { issues: 42, prs: 28, comments: 156 }
},
{
id: 2,
name: "李四",
openrank: 88.7,
change: 8.3,
activity: { issues: 35, prs: 32, comments: 198 }
},
{
id: 3,
name: "王五",
openrank: 82.1,
change: -3.2,
activity: { issues: 28, prs: 25, comments: 132 }
},
{
id: 4,
name: "赵六",
openrank: 76.5,
change: 15.7,
activity: { issues: 31, prs: 19, comments: 145 }
},
{
id: 5,
name: "钱七",
openrank: 68.9,
change: 5.4,
activity: { issues: 22, prs: 26, comments: 112 }
},
{
id: 6,
name: "孙八",
openrank: 62.3,
change: -2.1,
activity: { issues: 18, prs: 15, comments: 98 }
},
{
id: 7,
name: "周九",
openrank: 55.8,
change: 9.8,
activity: { issues: 25, prs: 12, comments: 87 }
},
{
id: 8,
name: "吴十",
openrank: 48.2,
change: -5.6,
activity: { issues: 15, prs: 8, comments: 65 }
},
{
id: 9,
name: "郑十一",
openrank: 41.7,
change: 3.2,
activity: { issues: 12, prs: 6, comments: 54 }
},
{
id: 10,
name: "王十二",
openrank: 35.4,
change: 7.1,
activity: { issues: 8, prs: 4, comments: 42 }
}
];
// 添加更多随机开发者
const randomDevelopers = Array.from({ length: 40 }, (_, i) => ({
id: i + 11,
name: `dev_${String.fromCharCode(65 + i)}`,
openrank: Math.random() * 100,
change: (Math.random() - 0.5) * 20,
activity: {
issues: Math.floor(Math.random() * 50),
prs: Math.floor(Math.random() * 30),
comments: Math.floor(Math.random() * 200)
}
}));
// 创建有意义的仓库示例数据
const exampleRepositories = [
{
id: 1,
name: "openrank-core",
openrank: 487.3,
stars: 1250,
forks: 342,
contributors: 45
},
{
id: 2,
name: "data-analytics",
openrank: 356.8,
stars: 876,
forks: 215,
contributors: 32
},
{
id: 3,
name: "web-dashboard",
openrank: 298.4,
stars: 654,
forks: 178,
contributors: 28
},
{
id: 4,
name: "api-service",
openrank: 245.1,
stars: 432,
forks: 123,
contributors: 22
},
{
id: 5,
name: "mobile-app",
openrank: 198.7,
stars: 321,
forks: 98,
contributors: 18
}
];
// 添加更多随机仓库
const randomRepositories = Array.from({ length: 15 }, (_, i) => ({
id: i + 6,
name: `project-${i + 1}`,
openrank: Math.random() * 500,
stars: Math.floor(Math.random() * 1000),
forks: Math.floor(Math.random() * 200),
contributors: Math.floor(Math.random() * 50) + 5
}));
return {
developers: [...exampleDevelopers, ...randomDevelopers],
repositories: [...exampleRepositories, ...randomRepositories],
activityDistribution: {
issueComment: 45.2,
openIssue: 23.8,
openPull: 18.5,
reviewComment: 8.7,
mergedPull: 3.8
},
openrankDistribution: [
{ range: "0-10", count: 5 },
{ range: "10-20", count: 8 },
{ range: "20-30", count: 12 },
{ range: "30-40", count: 15 },
{ range: "40-50", count: 18 },
{ range: "50-60", count: 22 },
{ range: "60-70", count: 16 },
{ range: "70-80", count: 12 },
{ range: "80-90", count: 8 },
{ range: "90-100", count: 4 }
]
};
}
// 加载模拟数据到界面
loadMockData() {
this.updateStats();
this.updateRankingTable();
this.updateRepositoryGrid();
}
// 更新统计信息
updateStats() {
const totalOpenrank = this.mockData.developers.reduce((sum, dev) => sum + dev.openrank, 0);
const avgContribution = totalOpenrank / this.mockData.developers.length;
document.getElementById('active-developers').textContent = this.mockData.developers.length;
document.getElementById('active-repos').textContent = this.mockData.repositories.length;
document.getElementById('total-openrank').textContent = totalOpenrank.toFixed(1);
document.getElementById('avg-contribution').textContent = avgContribution.toFixed(2);
}
// 更新排名表格
updateRankingTable() {
const rankingBody = document.getElementById('ranking-body');
const topDevelopers = [...this.mockData.developers]
.sort((a, b) => b.openrank - a.openrank)
.slice(0, 10);
rankingBody.innerHTML = topDevelopers.map((dev, index) => `
<div class="ranking-item">
<span class="rank rank-${index + 1}">${index + 1}</span>
<span>${dev.name}</span>
<span>${dev.openrank.toFixed(2)}</span>
<span class="stat-change ${dev.change >= 0 ? 'positive' : 'negative'}">
${dev.change >= 0 ? '+' : ''}${dev.change.toFixed(1)}%
</span>
</div>
`).join('');
}
// 更新仓库网格(增强版)
updateRepositoryGrid() {
const repoGrid = document.getElementById('repo-grid');
const sortedRepos = [...this.mockData.repositories].sort((a, b) => b.openrank - a.openrank);
repoGrid.innerHTML = sortedRepos.map((repo, index) => `
<div class="repo-card slide-in" style="animation-delay: ${index * 0.1}s">
<div class="repo-header">
<div class="repo-icon">
<i class="fas fa-code-branch"></i>
</div>
<div class="repo-info">
<h3>${repo.name}</h3>
<span class="repo-rank">排名: #${index + 1}</span>
</div>
<span class="repo-score">${repo.openrank.toFixed(1)}</span>
</div>
<div class="repo-metrics">
<div class="metric-row">
<div class="metric">
<i class="fas fa-star"></i>
<span class="metric-value">${repo.stars}</span>
<span class="metric-label">Stars</span>
</div>
<div class="metric">
<i class="fas fa-code-branch"></i>
<span class="metric-value">${repo.forks}</span>
<span class="metric-label">Forks</span>
</div>
<div class="metric">
<i class="fas fa-users"></i>
<span class="metric-value">${repo.contributors}</span>
<span class="metric-label">Contributors</span>
</div>
</div>
</div>
<div class="repo-progress">
<div class="progress-info">
<span>OpenRank 进度</span>
<span>${repo.openrank.toFixed(1)}/500</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${(repo.openrank / 500 * 100).toFixed(1)}%"></div>
</div>
</div>
<div class="repo-tags">
<span class="tag ${repo.openrank > 400 ? 'tag-success' : repo.openrank > 250 ? 'tag-warning' : 'tag-primary'}">
${repo.openrank > 400 ? '顶级项目' : repo.openrank > 250 ? '优秀项目' : '普通项目'}
</span>
<span class="tag ${repo.stars > 800 ? 'tag-success' : repo.stars > 400 ? 'tag-warning' : 'tag-primary'}">
${repo.stars > 800 ? '热门项目' : repo.stars > 400 ? '受欢迎' : '新兴项目'}
</span>
<span class="tag ${repo.contributors > 30 ? 'tag-success' : repo.contributors > 15 ? 'tag-warning' : 'tag-primary'}">
${repo.contributors > 30 ? '大型团队' : repo.contributors > 15 ? '中型团队' : '小型团队'}
</span>
</div>
<div class="repo-actions">
<button class="btn-secondary">
<i class="fas fa-chart-line"></i>
查看详情
</button>
<button class="btn-secondary">
<i class="fas fa-code"></i>
贡献分析
</button>
</div>
</div>
`).join('');
}
// 初始化图表
initCharts() {
this.initDistributionChart();
this.initActivityChart();
}
// 初始化分布图表
initDistributionChart() {
const ctx = document.getElementById('distribution-canvas').getContext('2d');
this.charts.distribution = new Chart(ctx, {
type: 'bar',
data: {
labels: this.mockData.openrankDistribution.map(d => d.range),
datasets: [{
label: '开发者数量',
data: this.mockData.openrankDistribution.map(d => d.count),
backgroundColor: 'rgba(0, 212, 255, 0.6)',
borderColor: 'rgba(0, 212, 255, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#ffffff'
}
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#ffffff'
}
},
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#ffffff'
}
}
}
}
});
}
// 初始化活动类型图表
initActivityChart() {
const ctx = document.getElementById('activity-canvas').getContext('2d');
this.charts.activity = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Issue 评论', '创建 Issue', '创建 PR', '代码评审', '合入 PR'],
datasets: [{
data: [
this.mockData.activityDistribution.issueComment,
this.mockData.activityDistribution.openIssue,
this.mockData.activityDistribution.openPull,
this.mockData.activityDistribution.reviewComment,
this.mockData.activityDistribution.mergedPull
],
backgroundColor: [
'rgba(0, 212, 255, 0.8)',
'rgba(0, 255, 136, 0.8)',
'rgba(255, 193, 7, 0.8)',
'rgba(156, 39, 176, 0.8)',
'rgba(255, 87, 34, 0.8)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
color: '#ffffff',
font: {
size: 12
}
}
}
}
}
});
}
// 初始化网络图谱
initNetworkGraph() {
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
// 设置画布尺寸
canvas.width = canvas.parentElement.offsetWidth;
canvas.height = canvas.parentElement.offsetHeight;
// 简单的网络图谱绘制
this.drawSimpleNetwork(ctx, canvas.width, canvas.height);
}
// 绘制基于OpenRank参数的复杂网络图谱
drawSimpleNetwork(ctx, width, height) {
ctx.clearRect(0, 0, width, height);
// 绘制背景网格
this.drawGrid(ctx, width, height);
// 获取配置参数用于网络图谱计算
const attenuationFactor = parseFloat(document.getElementById('attenuation-factor').value) || 0.85;
const developerRetention = parseFloat(document.getElementById('developer-retention').value) || 0.5;
const repoRetention = parseFloat(document.getElementById('repo-retention').value) || 0.3;
const issueCommentWeight = parseFloat(document.getElementById('issue-comment-weight').value) || 0.5252;
const openIssueWeight = parseFloat(document.getElementById('open-issue-weight').value) || 2.2235;
const openPullWeight = parseFloat(document.getElementById('open-pull-weight').value) || 4.0679;
// 基于配置参数生成节点数据
const nodes = [
{
x: width * 0.3,
y: height * 0.4,
size: 20 + (developerRetention * 30), // 开发者继承比例影响节点大小
color: '#00d4ff',
label: `User\nRet:${developerRetention}`,
type: 'User',
value: developerRetention * 100
},
{
x: width * 0.7,
y: height * 0.4,
size: 25 + (repoRetention * 25), // 仓库继承比例影响节点大小
color: '#00ff88',
label: `Repo\nRet:${repoRetention}`,
type: 'Repo',
value: repoRetention * 100
},
{
x: width * 0.5,
y: height * 0.2,
size: 18 + (issueCommentWeight * 5), // Issue评论权重影响节点大小
color: '#ffd700',
label: `Issue\nW:${issueCommentWeight}`,
type: 'Issue',
value: issueCommentWeight * 20
},
{
x: width * 0.5,
y: height * 0.6,
size: 22 + (openPullWeight * 3), // PR创建权重影响节点大小
color: '#ff6b6b',
label: `PR\nW:${openPullWeight}`,
type: 'PullRequest',
value: openPullWeight * 10
},
{
x: width * 0.2,
y: height * 0.2,
size: 15 + (openIssueWeight * 4), // Issue创建权重影响节点大小
color: '#9c27b0',
label: `OpenIssue\nW:${openIssueWeight}`,
type: 'Issue',
value: openIssueWeight * 8
}
];
// 基于衰减因子计算连接线权重和样式
const baseLineWidth = 2;
const lineWidthMultiplier = attenuationFactor * 3;
// 绘制连接线(基于算法参数)
const drawConnection = (source, target, weight, type) => {
ctx.beginPath();
ctx.moveTo(source.x, source.y);
ctx.lineTo(target.x, target.y);
// 根据权重设置线条样式
const lineWidth = baseLineWidth + (weight * lineWidthMultiplier);
const alpha = 0.3 + (weight * 0.4);
ctx.strokeStyle = type === 'activity' ?
`rgba(0, 212, 255, ${alpha})` :
`rgba(0, 255, 136, ${alpha})`;
ctx.lineWidth = lineWidth;
ctx.stroke();
// 绘制权重标签
const midX = (source.x + target.x) / 2;
const midY = (source.y + target.y) / 2;
ctx.fillStyle = '#ffffff';
ctx.font = '10px Inter';
ctx.textAlign = 'center';
ctx.fillText(weight.toFixed(2), midX, midY - 5);
};
// 绘制不同类型的连接关系
drawConnection(nodes[0], nodes[1], developerRetention, 'belong');
drawConnection(nodes[0], nodes[2], issueCommentWeight, 'activity');
drawConnection(nodes[0], nodes[3], openPullWeight, 'activity');
drawConnection(nodes[0], nodes[4], openIssueWeight, 'activity');
drawConnection(nodes[1], nodes[2], repoRetention * 0.5, 'reverse');
drawConnection(nodes[1], nodes[3], repoRetention * 0.7, 'reverse');
// 绘制节点(包含参数信息)
nodes.forEach(node => {
// 绘制节点外圈(表示参数值)
ctx.beginPath();
ctx.arc(node.x, node.y, node.size + 5, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255, 255, 255, 0.2)`;
ctx.lineWidth = 2;
ctx.stroke();
// 绘制节点主体
ctx.beginPath();
ctx.arc(node.x, node.y, node.size, 0, Math.PI * 2);
ctx.fillStyle = node.color;
ctx.fill();
// 绘制节点内圈(表示衰减因子影响)
ctx.beginPath();
ctx.arc(node.x, node.y, node.size * attenuationFactor, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 1;
ctx.stroke();
// 绘制节点标签
ctx.fillStyle = '#ffffff';
ctx.font = '10px Inter';
ctx.textAlign = 'center';
// 多行标签显示
const lines = node.label.split('\n');
lines.forEach((line, index) => {
ctx.fillText(line, node.x, node.y + node.size + 20 + (index * 14));
});
// 显示参数值
ctx.font = '9px Inter';
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.fillText(`Value: ${node.value.toFixed(1)}`, node.x, node.y + node.size + 40 + (lines.length * 14));
});
// 绘制参数说明图例
this.drawEnhancedGraphLegend(ctx, width, height, {
attenuationFactor,
developerRetention,
repoRetention,
issueCommentWeight,
openIssueWeight,
openPullWeight
});
// 绘制网络统计信息
this.drawNetworkStats(ctx, width, height, nodes);
}
// 绘制增强的图例(包含参数信息)
drawEnhancedGraphLegend(ctx, width, height, params) {
const legendItems = [
{ color: '#00d4ff', label: '开发者节点', desc: `继承比例: ${params.developerRetention}` },
{ color: '#00ff88', label: '仓库节点', desc: `继承比例: ${params.repoRetention}` },
{ color: '#ffd700', label: 'Issue节点', desc: `评论权重: ${params.issueCommentWeight}` },
{ color: '#ff6b6b', label: 'PR节点', desc: `创建权重: ${params.openPullWeight}` },
{ color: '#9c27b0', label: 'Issue创建', desc: `权重: ${params.openIssueWeight}` }
];
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillRect(width - 200, 20, 180, legendItems.length * 40 + 60);
// 绘制标题
ctx.fillStyle = '#00d4ff';
ctx.font = '12px Inter';
ctx.textAlign = 'left';
ctx.fillText('网络图谱参数说明', width - 190, 40);
// 绘制图例项
legendItems.forEach((item, index) => {
const yPos = 60 + index * 40;
// 绘制颜色标记
ctx.fillStyle = item.color;
ctx.beginPath();
ctx.arc(width - 190, yPos, 6, 0, Math.PI * 2);
ctx.fill();
// 绘制标签
ctx.fillStyle = '#ffffff';
ctx.font = '11px Inter';
ctx.fillText(item.label, width - 175, yPos + 4);
// 绘制参数描述
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '10px Inter';
ctx.fillText(item.desc, width - 175, yPos + 18);
});
// 绘制全局参数
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.font = '10px Inter';
ctx.fillText(`衰减因子: ${params.attenuationFactor}`, width - 190, legendItems.length * 40 + 50);
}
// 绘制网络统计信息
drawNetworkStats(ctx, width, height, nodes) {
const totalNodes = nodes.length;
const totalConnections = 6; // 手动计算的连接数
const avgNodeSize = nodes.reduce((sum, node) => sum + node.size, 0) / totalNodes;
const maxNodeValue = Math.max(...nodes.map(node => node.value));
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(20, height - 100, 200, 80);
ctx.fillStyle = '#ffffff';
ctx.font = '12px Inter';
ctx.textAlign = 'left';
ctx.fillText('网络统计信息', 30, height - 80);
ctx.font = '10px Inter';
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fillText(`节点数量: ${totalNodes}`, 30, height - 65);
ctx.fillText(`连接数量: ${totalConnections}`, 30, height - 50);
ctx.fillText(`平均节点大小: ${avgNodeSize.toFixed(1)}`, 30, height - 35);
ctx.fillText(`最大节点价值: ${maxNodeValue.toFixed(1)}`, 30, height - 20);
}
// 绘制网格背景
drawGrid(ctx, width, height) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 1;
// 水平线
for (let y = 0; y < height; y += 50) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
// 垂直线
for (let x = 0; x < width; x += 50) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
}
// 绘制图例
drawGraphLegend(ctx, width, height) {
const legend = [
{ color: '#00d4ff', label: '开发者' },
{ color: '#00ff88', label: '仓库' },
{ color: '#ffd700', label: 'Issue' },
{ color: '#ff6b6b', label: 'Pull Request' }
];
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(width - 150, 20, 130, legend.length * 25 + 20);
legend.forEach((item, index) => {
ctx.fillStyle = item.color;
ctx.beginPath();
ctx.arc(width - 130, 40 + index * 25, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = '12px Inter';
ctx.textAlign = 'left';
ctx.fillText(item.label, width - 120, 45 + index * 25);
});
}
// 过滤开发者
filterDevelopers(query) {
const filtered = this.mockData.developers.filter(dev =>
dev.name.toLowerCase().includes(query.toLowerCase())
);
this.displayDeveloperAnalysis(filtered);
}
// 显示开发者分析
displayDeveloperAnalysis(developers) {
const developerStats = document.querySelector('.developer-stats');
if (developers.length === 0) {
developerStats.innerHTML = `
<div class="no-results">
<i class="fas fa-search"></i>
<h3>未找到匹配的开发者</h3>
<p>请尝试其他搜索关键词</p>
</div>
`;
return;
}
// 对开发者进行排序按OpenRank降序
const sortedDevelopers = [...developers].sort((a, b) => b.openrank - a.openrank);
developerStats.innerHTML = `
<div class="developer-analysis-header">
<h3>开发者分析结果 (${developers.length} 人)</h3>
<div class="analysis-summary">
<span class="summary-item">
<i class="fas fa-crown"></i>
最高 OpenRank: ${Math.max(...developers.map(d => d.openrank)).toFixed(2)}
</span>
<span class="summary-item">
<i class="fas fa-chart-line"></i>
平均 OpenRank: ${(developers.reduce((sum, d) => sum + d.openrank, 0) / developers.length).toFixed(2)}
</span>
</div>
</div>
<div class="developers-grid">
${sortedDevelopers.map(dev => this.createDeveloperCard(dev)).join('')}
</div>
<div class="developer-charts">
<div class="chart-section">
<h4>OpenRank 分布</h4>
<div class="chart-container" style="height: 200px">
<canvas id="developer-distribution-chart"></canvas>
</div>
</div>
<div class="chart-section">
<h4>活动类型占比</h4>
<div class="chart-container" style="height: 200px">
<canvas id="developer-activity-chart"></canvas>
</div>
</div>
</div>
`;
// 初始化开发者分析图表
this.initDeveloperCharts(developers);
}
// 创建开发者卡片
createDeveloperCard(developer) {
const activityTotal = developer.activity.issues + developer.activity.prs + developer.activity.comments;
const issuesPercent = activityTotal > 0 ? (developer.activity.issues / activityTotal * 100).toFixed(1) : 0;
const prsPercent = activityTotal > 0 ? (developer.activity.prs / activityTotal * 100).toFixed(1) : 0;
const commentsPercent = activityTotal > 0 ? (developer.activity.comments / activityTotal * 100).toFixed(1) : 0;
return `
<div class="developer-card slide-in">
<div class="developer-header">
<div class="developer-avatar">
<i class="fas fa-user"></i>
</div>
<div class="developer-info">
<h4>${developer.name}</h4>
<span class="developer-rank">OpenRank: ${developer.openrank.toFixed(2)}</span>
</div>
<span class="rank-badge">#${this.mockData.developers.sort((a, b) => b.openrank - a.openrank).findIndex(d => d.id === developer.id) + 1}</span>
</div>
<div class="developer-stats">
<div class="stat-row">
<span class="stat-label">Issues</span>
<span class="stat-value">${developer.activity.issues}</span>
<div class="progress-bar">
<div class="progress-fill" style="width: ${issuesPercent}%"></div>
</div>
</div>
<div class="stat-row">
<span class="stat-label">PRs</span>
<span class="stat-value">${developer.activity.prs}</span>
<div class="progress-bar">
<div class="progress-fill" style="width: ${prsPercent}%"></div>
</div>
</div>
<div class="stat-row">
<span class="stat-label">Comments</span>
<span class="stat-value">${developer.activity.comments}</span>
<div class="progress-bar">
<div class="progress-fill" style="width: ${commentsPercent}%"></div>
</div>
</div>
</div>
<div class="developer-metrics">
<div class="metric">
<span class="metric-label">活跃度</span>
<span class="metric-value">${activityTotal}</span>
</div>
<div class="metric">
<span class="metric-label">变化率</span>
<span class="metric-value ${developer.change >= 0 ? 'positive' : 'negative'}">
${developer.change >= 0 ? '+' : ''}${developer.change.toFixed(1)}%
</span>
</div>
</div>
<div class="developer-tags">
<span class="tag ${developer.openrank > 80 ? 'tag-success' : developer.openrank > 50 ? 'tag-warning' : 'tag-primary'}">
${developer.openrank > 80 ? '核心贡献者' : developer.openrank > 50 ? '活跃开发者' : '普通贡献者'}
</span>
<span class="tag ${activityTotal > 100 ? 'tag-success' : activityTotal > 50 ? 'tag-warning' : 'tag-primary'}">
${activityTotal > 100 ? '高度活跃' : activityTotal > 50 ? '中等活跃' : '低活跃度'}
</span>
</div>
</div>
`;
}
// 初始化开发者分析图表
initDeveloperCharts(developers) {
// OpenRank 分布图表
const distCtx = document.getElementById('developer-distribution-chart').getContext('2d');
const openrankValues = developers.map(d => d.openrank);
new Chart(distCtx, {
type: 'bar',
data: {
labels: developers.map(d => d.name),
datasets: [{
label: 'OpenRank 值',
data: openrankValues,
backgroundColor: openrankValues.map(val =>
val > 80 ? 'rgba(0, 255, 136, 0.6)' :
val > 50 ? 'rgba(255, 193, 7, 0.6)' :
'rgba(0, 212, 255, 0.6)'
),
borderColor: openrankValues.map(val =>
val > 80 ? 'rgba(0, 255, 136, 1)' :
val > 50 ? 'rgba(255, 193, 7, 1)' :
'rgba(0, 212, 255, 1)'
),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (context) => `OpenRank: ${context.raw.toFixed(2)}`
}
}
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(255, 255, 255, 0.1)' },
ticks: { color: '#ffffff' }
},
x: {
grid: { display: false },
ticks: {
color: '#ffffff',
maxRotation: 45,
minRotation: 45
}
}
}
}
});
// 活动类型占比图表
const activityCtx = document.getElementById('developer-activity-chart').getContext('2d');
const totalIssues = developers.reduce((sum, d) => sum + d.activity.issues, 0);
const totalPRs = developers.reduce((sum, d) => sum + d.activity.prs, 0);
const totalComments = developers.reduce((sum, d) => sum + d.activity.comments, 0);
new Chart(activityCtx, {
type: 'doughnut',
data: {
labels: ['Issues', 'PRs', 'Comments'],
datasets: [{
data: [totalIssues, totalPRs, totalComments],
backgroundColor: [
'rgba(0, 212, 255, 0.8)',
'rgba(0, 255, 136, 0.8)',
'rgba(255, 193, 7, 0.8)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#ffffff' }
}
}
}
});
}
// 过滤仓库
filterRepositories(filterType) {
let filteredRepos = [...this.mockData.repositories];
switch (filterType) {
case 'top10':
filteredRepos = filteredRepos.sort((a, b) => b.openrank - a.openrank).slice(0, 10);
break;
case 'rising':
// 模拟上升最快的仓库(按某种指标排序)
filteredRepos = filteredRepos.sort((a, b) => b.stars - a.stars).slice(0, 10);
break;
}
this.displayFilteredRepositories(filteredRepos);
}
// 显示过滤后的仓库
displayFilteredRepositories(repos) {
const repoGrid = document.getElementById('repo-grid');
repoGrid.innerHTML = repos.map(repo => `
<div class="repo-card">
<h3>${repo.name}</h3>
<div class="repo-stats">
<div>OpenRank: <strong>${repo.openrank.toFixed(2)}</strong></div>
<div>Stars: ${repo.stars}</div>
<div>Forks: ${repo.forks}</div>
<div>Contributors: ${repo.contributors}</div>
</div>
</div>
`).join('');
}
// 更新配置
updateConfig(settingId, value) {
console.log('Config updated:', settingId, value);
// 这里可以添加配置保存逻辑
}
// 计算 OpenRank模拟
calculateOpenRank() {
this.showLoading();
// 模拟计算过程
setTimeout(() => {
this.hideLoading();
this.refreshData();
this.showNotification('OpenRank 计算完成!', 'success');
}, 2000);
}
// 显示加载动画
showLoading() {
document.getElementById('loading-overlay').classList.add('active');
}
// 隐藏加载动画
hideLoading() {
document.getElementById('loading-overlay').classList.remove('active');
}
// 刷新数据
refreshData() {
this.mockData = this.generateMockData();
this.loadMockData();
// 更新图表
if (this.charts.distribution) {
this.charts.distribution.data.datasets[0].data =
this.mockData.openrankDistribution.map(d => d.count);
this.charts.distribution.update();
}
if (this.charts.activity) {
this.charts.activity.data.datasets[0].data = [
this.mockData.activityDistribution.issueComment,
this.mockData.activityDistribution.openIssue,
this.mockData.activityDistribution.openPull,
this.mockData.activityDistribution.reviewComment,
this.mockData.activityDistribution.mergedPull
];
this.charts.activity.update();
}
}
// 显示通知
showNotification(message, type = 'info') {
// 简单的通知实现
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 1001;
background: ${type === 'success' ? '#00ff88' : '#00d4ff'};
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// 更新最后更新时间
updateLastUpdateTime() {
const now = new Date();
document.getElementById('last-update').textContent =
now.toLocaleString('zh-CN');
}
}
// 页面加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => {
new OpenRankDashboard();
});
// 工具函数
function formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}