Comments
-
${developer.activity.comments}
+
${commentsCount}
@@ -1007,16 +1535,156 @@ class OpenRankDashboard {
// 计算 OpenRank(模拟)
calculateOpenRank() {
- this.showLoading();
-
- // 模拟计算过程
- setTimeout(() => {
- this.hideLoading();
- this.refreshData();
- this.showNotification('OpenRank 计算完成!', 'success');
- }, 2000);
+ const ownerInput = document.getElementById('calc-owner');
+ const repoInput = document.getElementById('calc-repo');
+ const owner = ownerInput ? ownerInput.value.trim() : '';
+ const repo = repoInput ? repoInput.value.trim() : '';
+ const startInput = document.getElementById('calc-start');
+ const endInput = document.getElementById('calc-end');
+ const startVal = startInput && startInput.value ? startInput.value : '';
+ const endVal = endInput && endInput.value ? endInput.value : '';
+ const useExplicitDates = startVal && endVal;
+ if (!owner || !repo) {
+ const btn = document.getElementById('calculate-btn');
+ if (btn) {
+ const old = btn.dataset.originalText || btn.textContent;
+ btn.dataset.originalText = old;
+ btn.textContent = '请输入 owner 和 repo 再计算';
+ btn.disabled = true;
+ setTimeout(()=>{ if (btn){ btn.textContent = old; btn.disabled = false; } }, 1800);
+ }
+ this.showNotification('请输入 owner 和 repo 再计算', 'negative');
+ return;
+ }
+ this.showLoading();
+ const payload = { owner, repo, topNUsers: 50, topNRepos: 20, useReal: true };
+ if (useExplicitDates) {
+ payload.startDate = new Date(startVal + 'T00:00:00Z').toISOString();
+ const endDateObj = new Date(endVal + 'T00:00:00Z');
+ endDateObj.setUTCDate(endDateObj.getUTCDate() + 1);
+ endDateObj.setUTCSeconds(endDateObj.getUTCSeconds() - 1);
+ payload.endDate = endDateObj.toISOString();
+ } else {
+ // 默认最近 30 天
+ payload.days = 30;
+ }
+ fetch('http://localhost:4001/api/calculate', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ })
+ .then(async resp => {
+ if (!resp.ok) {
+ const err = await resp.json().catch(()=>({}));
+ throw new Error(err.error || resp.statusText || '计算失败');
+ }
+ return resp.json();
+ })
+ .then(data => {
+ if (data && data.results) {
+ const r = data.results;
+ this.mockData.developers = (r.users || []).map(u => ({ id: u.id, name: u.name, openrank: u.openrank||0, change: u.change||0, activity: u.activity || { issues:0, prs:0, comments:0 } }));
+ this.mockData.repositories = (r.repos || []).map(p => ({ id: p.id, name: p.name, openrank: p.openrank||0, stars: p.stars||0, forks: p.forks||0, contributors: p.contributors||0 }));
+ if (Array.isArray(r.openrankDistribution) && r.openrankDistribution.length) this.mockData.openrankDistribution = r.openrankDistribution;
+ if (r.activityDistribution && Object.keys(r.activityDistribution).length) this.mockData.activityDistribution = r.activityDistribution;
+ this.repoContributors = r.repoContributors || {};
+ const sumActivity = (this.mockData.developers||[]).reduce((s,d)=> s + (d.activity.issues||0)+(d.activity.prs||0)+(d.activity.comments||0),0);
+ if (sumActivity === 0) {
+ this.showNotification('警告:本次计算活动总量为 0,可能没有抓到真实数据或窗口内为空', 'negative');
+ }
+ }
+ this.hideLoading();
+ this.loadMockData();
+ try {
+ if (this.charts.distribution) {
+ const labels = (this.mockData.openrankDistribution || []).map(d=>d.range);
+ const counts = (this.mockData.openrankDistribution || []).map(d=>d.count);
+ this.charts.distribution.data.labels = labels;
+ if (this.charts.distribution.data.datasets && this.charts.distribution.data.datasets[0]) {
+ this.charts.distribution.data.datasets[0].data = counts;
+ }
+ this.charts.distribution.update();
+ }
+ if (this.charts.activity && this.mockData.activityDistribution) {
+ 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();
+ }
+ } catch (e) { console.warn('chart update failed', e); }
+ if (this.mode === 'project') {
+ this.projectState.list = []; // clear to force reload
+ this.buildProjectListFromGlobal();
+ }
+ const winDays = (data && data.window && data.window.days) ? data.window.days : (payload.days|| (useExplicitDates ? Math.round((new Date(payload.endDate)-new Date(payload.startDate))/86400000) : 30));
+ const winLabel = useExplicitDates ? `${payload.startDate.slice(0,10)} ~ ${payload.endDate.slice(0,10)} (${winDays}天)` : `最近 ${winDays} 天`;
+ this.showNotification('OpenRank 计算完成!窗口: ' + winLabel, 'success');
+ this.closeCalcModal();
+ // 自动切换到项目模式并高亮新仓库
+ this.switchMode('project');
+ const fullName = owner + '/' + repo;
+ this.projectState.current = fullName;
+ // 延迟等待列表刷新
+ setTimeout(()=>{
+ this.renderProjectList();
+ if (!this.projectState.approxSnapshots[fullName]) this.renderApproxProject(fullName, winDays);
+ }, 600);
+ })
+ .catch(err => {
+ console.error('计算失败', err);
+ this.hideLoading();
+ this.showNotification('OpenRank 计算失败: ' + (err.message || err), 'negative');
+ });
}
+ openCalcModal() {
+ const modal = document.getElementById('calc-modal');
+ if (!modal) return;
+ modal.classList.add('open');
+ const ownerInput = document.getElementById('calc-owner');
+ if (ownerInput) ownerInput.focus();
+ // bind close buttons
+ modal.querySelectorAll('[data-close-calc]').forEach(btn => btn.addEventListener('click', () => this.closeCalcModal(), { once: true }));
+ // form submit
+ const form = document.getElementById('calc-form');
+ if (form) {
+ form.addEventListener('submit', (e) => {
+ e.preventDefault();
+ this.calculateOpenRank();
+ }, { once: true });
+ }
+ // quick range buttons
+ const startInput = document.getElementById('calc-start');
+ const endInput = document.getElementById('calc-end');
+ const quickBtns = modal.querySelectorAll('.quick-range-btn');
+ if (quickBtns && startInput && endInput) {
+ quickBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const days = parseInt(btn.getAttribute('data-range')||'0', 10);
+ if (!days || days <= 0) return;
+ // remove previous active
+ quickBtns.forEach(b=>b.classList.remove('active'));
+ btn.classList.add('active');
+ const today = new Date();
+ const endDate = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
+ const startDate = new Date(endDate.getTime() - (days - 1) * 86400000);
+ const fmt = d => d.toISOString().slice(0,10);
+ startInput.value = fmt(startDate);
+ endInput.value = fmt(endDate);
+ }, { once: true }); // once per modal open
+ });
+ }
+ }
+
+ closeCalcModal() {
+ const modal = document.getElementById('calc-modal');
+ if (modal) modal.classList.remove('open');
+ }
+
// 显示加载动画
showLoading() {
document.getElementById('loading-overlay').classList.add('active');
@@ -1076,6 +1744,178 @@ class OpenRankDashboard {
}, 3000);
}
+ // Fetch and show repo details modal
+ async showRepoDetails(owner, repoName) {
+ // create modal container if not exist
+ let modal = document.getElementById('repo-detail-modal');
+ if (!modal) {
+ modal = document.createElement('div');
+ modal.id = 'repo-detail-modal';
+ modal.className = 'modal';
+ modal.innerHTML = `
+
+ `;
+ document.body.appendChild(modal);
+ modal.querySelector('.modal-close').addEventListener('click', () => modal.classList.remove('open'));
+ }
+
+ // show loading state
+ const title = modal.querySelector('#repo-detail-title');
+ const summary = modal.querySelector('#repo-detail-summary');
+ const contribContainer = modal.querySelector('#repo-detail-contributors');
+ const canvas = modal.querySelector('#repo-detail-timeseries');
+ title.textContent = `仓库:${owner}/${repoName}`;
+ summary.innerHTML = `
加载中…
`;
+ contribContainer.innerHTML = '';
+
+ modal.classList.add('open');
+
+ try {
+ const params = new URLSearchParams({ start: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30).toISOString(), end: new Date().toISOString(), granularity: 'day', useCache: '1' });
+ // ensure owner and repo are separate path segments (do not include a slash in repoName)
+ const resp = await fetch(`http://localhost:4001/api/repo/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/details?${params.toString()}`);
+ if (!resp.ok) {
+ const err = await resp.json().catch(()=>({}));
+ throw new Error(err.error || resp.statusText || '无法获取仓库详情');
+ }
+ const data = await resp.json();
+ // repository window summary
+ const winStart = data.window && data.window.start ? data.window.start : '';
+ const winEnd = data.window && data.window.end ? data.window.end : '';
+ // repo metadata (be defensive against different field names)
+ const repoMeta = data.repo || data.repository || {};
+ const fullName = repoMeta.full_name || repoMeta.fullName || `${owner}/${repoName}`;
+ const stars = repoMeta.stars || repoMeta.stargazers_count || repoMeta.stargazers || 0;
+ const forks = repoMeta.forks || repoMeta.forks_count || 0;
+ const repoDescription = repoMeta.description || '';
+
+ summary.innerHTML = `
+
+
+
+
${fullName}
+
${repoDescription}
+
时间窗口: ${winStart} — ${winEnd}
+
+
+
+
Stars
+
${formatNumber(stars)}
+
+
+
Forks
+
${formatNumber(forks)}
+
+
+
Contributors
+
${(data.contributors && data.contributors.length) || '-'}
+
+
+
+
+ `;
+
+ // Build a compact contributors section (top 10) and an activity breakdown area
+ const contributors = (data.contributors || []).slice(0, 10);
+ if (!contributors || contributors.length === 0) {
+ contribContainer.innerHTML = `
未找到贡献者数据。
`;
+ } else {
+ const rows = contributors.map(c => `
+
+ | ${c.name || c.actor_login || 'unknown'} |
+ ${(c.activity && (c.activity.issues||0)) || (c.issues||0)} |
+ ${(c.activity && (c.activity.prs||0)) || (c.prs||0)} |
+ ${(c.activity && (c.activity.comments||0)) || (c.comments||0)} |
+ ${c.merged || 0} |
+
+ `).join('');
+ contribContainer.innerHTML = `
+
Top 贡献者(最多 10 人)
+
+ | 名称 | Issues | PRs | Comments | Merged |
+ ${rows}
+
+ `;
+ }
+
+ // compute aggregated activity for breakdown (fallback to timeseries sums)
+ const ts = data.timeseries || [];
+ const labels = ts.map(p=>p.period);
+ const issuesData = ts.map(p=>p.issues || 0);
+ const prsData = ts.map(p=>p.prs || 0);
+ const mergedData = ts.map(p=>p.merged || 0);
+ const commentsData = ts.map(p=>p.comments || 0);
+
+ const totalIssues = issuesData.reduce((a,b)=>a+b,0);
+ const totalPRs = prsData.reduce((a,b)=>a+b,0);
+ const totalMerged = mergedData.reduce((a,b)=>a+b,0);
+ const totalComments = commentsData.reduce((a,b)=>a+b,0);
+
+ // activity breakdown canvas (insert above timeseries if not present)
+ let breakdownCanvas = modal.querySelector('#repo-activity-breakdown');
+ if (!breakdownCanvas) {
+ const el = document.createElement('div');
+ el.innerHTML = `
`;
+ // replace existing canvas area
+ canvas.parentElement.parentElement.insertBefore(el, canvas.parentElement);
+ // remove original single-canvas wrapper
+ canvas.parentElement.remove();
+ }
+
+ // cleanup existing chart instances
+ if (this.charts.repoTimeseries) { try { this.charts.repoTimeseries.destroy(); } catch(e){} }
+ if (this.charts.repoActivityBreakdown) { try { this.charts.repoActivityBreakdown.destroy(); } catch(e){} }
+
+ // create breakdown chart
+ const breakdownCtx = (modal.querySelector('#repo-activity-breakdown') || document.getElementById('repo-activity-breakdown')).getContext('2d');
+ this.charts.repoActivityBreakdown = new Chart(breakdownCtx, {
+ type: 'doughnut',
+ data: {
+ labels: ['Issues','PRs','Merged','Comments'],
+ datasets: [{ data: [totalIssues, totalPRs, totalMerged, totalComments], backgroundColor: ['#00d4ff','#00ff88','#ffd700','#ff7ab6'] }]
+ },
+ options: { responsive:true, maintainAspectRatio:false, plugins:{legend:{position:'right', labels:{color:'#fff'}}} }
+ });
+
+ // create timeseries chart
+ const tsCanvas = modal.querySelector('#repo-detail-timeseries') || canvas;
+ const ctxTs = tsCanvas.getContext('2d');
+ this.charts.repoTimeseries = new Chart(ctxTs, {
+ type: 'line',
+ data: {
+ labels,
+ datasets: [
+ { label: 'Issues', data: issuesData, borderColor: '#00d4ff', fill:false },
+ { label: 'PRs', data: prsData, borderColor: '#00ff88', fill:false },
+ { label: 'Merged', data: mergedData, borderColor: '#ffd700', fill:false }
+ ]
+ },
+ options: { responsive:true, maintainAspectRatio:false }
+ });
+
+ } catch (err) {
+ console.error('showRepoDetails error', err);
+ summary.innerHTML = `
无法获取仓库详情: ${(err && err.message) || err}
`;
+ }
+ }
+
// 更新最后更新时间
updateLastUpdateTime() {
const now = new Date();
diff --git a/front/frontend/index.html b/front/frontend/index.html
index 9ff4028..7669d94 100644
--- a/front/frontend/index.html
+++ b/front/frontend/index.html
@@ -7,6 +7,8 @@
+
+
@@ -16,6 +18,10 @@
OpenRank Analytics
+
+
+
+