|
|
// 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);
|
|
|
};
|
|
|
}
|