// ==UserScript==
// @name 研发云统计 (v4.0: 中文UI+美化+Excel导出+可编辑系数)
// @namespace http://tampermonkey.net/
// @version 4.0
// @description 快捷月份、跨页统计、趋势图(多数据集)、导出Excel(.xlsx)、后缀折算系数可编辑并保存;中文UI与工具条美化;去掉进度条与100%显示。
// @author Gemini & yy &GPT &yf
// @match https://devcloud.aspirecn.com/index.php?m=my&f=mycommitlog*
// @grant none
// @icon https://www.google.com/s2/favicons?sz=64&domain=10.1.6.39
// ==/UserScript==
(function () {
'use strict';
// --- ⚙️ 配置区域 ---
const QUERY_BUTTON_SELECTOR = 'button#submit';
const TABLE_SELECTOR = 'table.table-list';
const PAGER_CONTAINER_SELECTOR = 'div.table-footer .pager';
const DATE_COLUMN_INDEX = 2,
SUFFIX_COLUMN_INDEX = 4,
ADDED_COLUMN_INDEX = 6,
DELETED_COLUMN_INDEX = 7,
IS_VALID_COLUMN_INDEX = 8;
const DEFAULT_CONVERSION_RATES = {
java:1, h:1.3, m:1, mm:1, xib:1, htm:0.1, html:0.1, tpl:1, shtml:0.1, css:0.1,
js:0.7, less:1, php:1, jsp:0.1, c:1.3, cpp:1.3, xml:0.5, sql:1, other:0.1,
bshrc:0.3, jmx:0.5, vue:0.7, sh:1, sol:1.3, yml:1, yaml:1, go:1.2, py:1, json:0.1,
dart:1, swift:1.3, kt:1, ts:0.7, sass:0.4, scala:1, lua:1, wpy:1, cs:1, ftl:1,
properties:1, rb:1, scss:0.1, vm:1, nvue:0.8
};
const RATES_STORE_KEY = 'codeStatsRates';
// --- 配置结束 ---
const LOG_PREFIX = '[代码统计脚本 v4.0]:';
const SESSION_STORAGE_KEY_DATA = 'codeStatsDataV40';
const SESSION_STORAGE_KEY_FLAG = 'isCollectingCodeStats';
let isProcessing = false;
let runtime = {
byDate: {}, // { 'YYYY-MM-DD': { commitCount, rawAdded, rawDeleted } }
byDateSuffix: {}, // { date: { suffix: { rawAdded, rawDeleted } } }
};
// 折算系数
function loadRates(){
try{ const saved=JSON.parse(localStorage.getItem(RATES_STORE_KEY)||'{}'); return {...DEFAULT_CONVERSION_RATES, ...saved}; }
catch{ return {...DEFAULT_CONVERSION_RATES}; }
}
function saveRates(newPart){
const saved=JSON.parse(localStorage.getItem(RATES_STORE_KEY)||'{}');
localStorage.setItem(RATES_STORE_KEY, JSON.stringify({...saved, ...newPart}));
}
function getRateForSuffix(suffix, rates){
if (['shtml','html','htm','jsp'].includes(suffix)) return 0.1;
return rates[suffix] ?? rates['other'] ?? 0.1;
}
// 外部库
function loadChartJs(){
return new Promise((res,rej)=>{ if(window.Chart) return res(); const s=document.createElement('script'); s.src='https://cdn.jsdelivr.net/npm/chart.js'; s.onload=res; s.onerror=rej; document.head.appendChild(s); });
}
function loadSheetJS(){
return new Promise((res,rej)=>{ if(window.XLSX) return res(); const s=document.createElement('script'); s.src='https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js'; s.onload=res; s.onerror=rej; document.head.appendChild(s); });
}
let chartInstance=null;
// 创建UI
function createUI(){
createModal();
// 小样式(工具条/按钮/加载动画)
const style = document.createElement('style');
style.textContent = `
.cs-toolbar{background:#fafbfc;border:1px solid #eaecef;border-radius:12px;padding:10px 12px;box-shadow:0 1px 2px rgba(0,0,0,.03);display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px}
.cs-btn{border:1px solid #e0e0e0;background:#fff;border-radius:10px;padding:6px 12px;cursor:pointer;line-height:1.2}
.cs-btn:hover{background:#f4f6f8}
.cs-btn-primary{background:#1976d2;color:#fff;border-color:#1976d2}
.cs-btn-primary:hover{filter:brightness(1.05)}
.cs-chip{display:inline-block;padding:2px 8px;border-radius:999px;background:#eef2ff;color:#3f51b5;font-size:12px}
.cs-section{border:1px solid #eee;border-radius:12px;padding:10px;margin-bottom:12px}
.cs-flex{display:flex;align-items:center;gap:12px}
.cs-inline{display:inline-flex;align-items:center;gap:8px}
.cs-divider{width:1px;height:18px;background:#ddd;display:inline-block}
.cs-spinner{width:16px;height:16px;border:2px solid #e0e0e0;border-top-color:#1976d2;border-radius:50%;animation:cs-spin 1s linear infinite}
@keyframes cs-spin{to{transform:rotate(360deg)}}
`;
document.head.appendChild(style);
const queryButton=document.querySelector(QUERY_BUTTON_SELECTOR);
if(!queryButton) return;
const queryButtonContainer=queryButton.closest('.row');
if(!queryButtonContainer) return;
// 统计按钮
const wrapper=document.createElement('div');
wrapper.className='col-sm-4';
const btn=document.createElement('button');
btn.id='start-stats-button';
btn.className='btn';
btn.title='统计当前手动选择日期范围内的代码量';
btn.textContent='📊 统计(当前范围)';
btn.style.cssText='background:#6c757d;color:#fff;border:none;padding:5px 12px;border-radius:10px;box-shadow:0 2px 4px rgba(0,0,0,.08);';
btn.onclick=startFullStats;
wrapper.appendChild(btn);
queryButtonContainer.appendChild(wrapper);
// 快捷月份
const conditionsDiv=document.getElementById('conditions');
if(conditionsDiv){
const now=new Date(); let monthOptions='';
for(let i=0;i<12;i++){ const d=new Date(now.getFullYear(), now.getMonth()-i,1); const y=d.getFullYear(); const m=d.getMonth()+1; monthOptions+=`<option value="${y}-${m}">${y}年${m}月</option>`; }
const quick=document.createElement('div');
quick.className='pull-right'; quick.style.display='flex'; quick.style.alignItems='center';
quick.innerHTML=`
<span style="font-weight:bold;margin-right:10px;">快捷月份:</span>
<button type="button" id="stat-this-month" class="cs-btn">本月</button>
<button type="button" id="stat-last-month" class="cs-btn">上月</button>
<select id="stat-select-month" class="form-control" style="display:inline-block;width:120px;margin-left:10px;height:30px;padding:4px 8px;"></select>
<button type="button" id="stat-by-selected-month" class="cs-btn cs-btn-primary">按月份统计</button>
`;
conditionsDiv.appendChild(quick);
document.getElementById('stat-select-month').innerHTML=monthOptions;
document.getElementById('stat-this-month').onclick=()=>{ const t=new Date(); startMonthlyStats(t.getFullYear(), t.getMonth()+1); };
document.getElementById('stat-last-month').onclick=()=>{ const t=new Date(); const lm=new Date(t.getFullYear(), t.getMonth()-1,1); startMonthlyStats(lm.getFullYear(), lm.getMonth()+1); };
document.getElementById('stat-by-selected-month').onclick=()=>{ const [y,m]=document.getElementById('stat-select-month').value.split('-').map(Number); startMonthlyStats(y,m); };
}
}
// Excel导出
async function exportExcel(sheets, fileName='CodeStats.xlsx'){
try{ await loadSheetJS(); }catch(e){ alert('Excel 库加载失败'); return; }
const wb=XLSX.utils.book_new();
sheets.forEach(sh=>{ const ws=XLSX.utils.aoa_to_sheet(sh.rows); XLSX.utils.book_append_sheet(wb, ws, sh.name); });
XLSX.writeFile(wb, fileName); // 文件名保持 ASCII,避免下载异常
}
// 主流程
function startMonthlyStats(year, month){
const s=new Date(year, month-1, 1), e=new Date(year, month, 0);
const fmt=d=>`${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}`;
sessionStorage.setItem(SESSION_STORAGE_KEY_DATA, JSON.stringify({byDate:{}, byDateSuffix:{}}));
sessionStorage.setItem(SESSION_STORAGE_KEY_FLAG, 'true');
window.location.href = createLink('my','mycommitlog', `startDate=${fmt(s)}&endDate=${fmt(e)}`);
}
async function startFullStats(){
if(isProcessing){ alert('正在统计,请稍候…'); return; }
sessionStorage.setItem(SESSION_STORAGE_KEY_DATA, JSON.stringify({byDate:{}, byDateSuffix:{}}));
sessionStorage.setItem(SESSION_STORAGE_KEY_FLAG, 'true');
const { currentPage }=getPagerInfo();
const statsButton=document.getElementById('start-stats-button');
const modal=document.getElementById('stats-modal-container');
if(currentPage===1){
isProcessing=true;
modal.style.display='flex';
if(statsButton){ statsButton.textContent='统计中…'; statsButton.disabled=true; }
await processCurrentPage();
}else{
modal.style.display='flex';
updateStatus('正在前往第一页…');
if(statsButton){ statsButton.textContent='导航中…'; statsButton.disabled=true; }
const first=findFirstPageButton();
if(first) first.click();
else{ alert('无法找到“首页”按钮,请手动返回第一页后重试。'); cleanupAfterFinish(); }
}
}
function findFirstPageButton(){ const links=document.querySelectorAll(`${PAGER_CONTAINER_SELECTOR} a`); for(const a of links){ if(a.querySelector('i.icon-first-page')) return a; } return null; }
async function processCurrentPage(){
const { currentPage, totalPages }=getPagerInfo();
updateStatus(`正在处理第 ${currentPage} / ${totalPages} 页…`);
const stored=JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEY_DATA)||'{}');
const byDate=stored.byDate||{}, byDateSuffix=stored.byDateSuffix||{};
parseTableData(currentPage, byDate, byDateSuffix);
sessionStorage.setItem(SESSION_STORAGE_KEY_DATA, JSON.stringify({byDate, byDateSuffix}));
const next=findNextPageButton();
if(currentPage<totalPages && next){ next.click(); }
else{ displayFinalResults(); cleanupAfterFinish(); }
}
function findNextPageButton(){ const links=document.querySelectorAll(`${PAGER_CONTAINER_SELECTOR} a`); for(const a of links){ if(a.querySelector('i.icon-angle-right')) return a; } return null; }
function cleanupAfterFinish(){
sessionStorage.removeItem(SESSION_STORAGE_KEY_FLAG);
isProcessing=false;
const statsButton=document.getElementById('start-stats-button');
if(statsButton){ statsButton.textContent='📊 统计(当前范围)'; statsButton.disabled=false; }
}
function parseTableData(currentPage, byDate, byDateSuffix){
const table=document.querySelector(TABLE_SELECTOR);
if(!table){ console.error(LOG_PREFIX, `第 ${currentPage} 页未找到表格`); return; }
table.querySelectorAll('tbody > tr').forEach(tr=>{
const tds=tr.querySelectorAll('td');
if(tds.length < IS_VALID_COLUMN_INDEX+1 || tds[IS_VALID_COLUMN_INDEX].textContent.trim()!=='有效') return;
const dateStr=(tds[DATE_COLUMN_INDEX].textContent.trim().split(' ')[0])||'';
const suffix=(tds[SUFFIX_COLUMN_INDEX].textContent.trim().toLowerCase()||'unknown');
const add=parseInt(tds[ADDED_COLUMN_INDEX].textContent.trim(),10)||0;
const del=parseInt(tds[DELETED_COLUMN_INDEX].textContent.trim(),10)||0;
if(!dateStr) return;
if(!byDate[dateStr]) byDate[dateStr]={commitCount:0, rawAdded:0, rawDeleted:0};
byDate[dateStr].commitCount++; byDate[dateStr].rawAdded+=add; byDate[dateStr].rawDeleted+=del;
if(!byDateSuffix[dateStr]) byDateSuffix[dateStr]={};
if(!byDateSuffix[dateStr][suffix]) byDateSuffix[dateStr][suffix]={rawAdded:0, rawDeleted:0};
byDateSuffix[dateStr][suffix].rawAdded+=add; byDateSuffix[dateStr][suffix].rawDeleted+=del;
});
}
// 聚合/计算
function computeFinal(byDate, byDateSuffix, rates){
const byDateComputed={}, bySuffixComputed={};
for(const date of Object.keys(byDate)){
const base=byDate[date]; let finalSum=0;
const mp=byDateSuffix[date]||{};
for(const [suf,vals] of Object.entries(mp)){
const rate=getRateForSuffix(suf, rates);
const fin=vals.rawAdded*rate + vals.rawDeleted*0.1;
finalSum+=fin;
if(!bySuffixComputed[suf]) bySuffixComputed[suf]={rawAdded:0, rawDeleted:0, finalLines:0};
bySuffixComputed[suf].rawAdded+=vals.rawAdded;
bySuffixComputed[suf].rawDeleted+=vals.rawDeleted;
bySuffixComputed[suf].finalLines+=fin;
}
byDateComputed[date]={commitCount:base.commitCount, rawAdded:base.rawAdded, rawDeleted:base.rawDeleted, finalLines:finalSum};
}
return {byDateComputed, bySuffixComputed};
}
function aggregateDaySeries(byDateComputed){
const arr=Object.entries(byDateComputed).sort((a,b)=> new Date(a[0]) - new Date(b[0]));
return {
labels: arr.map(e=>e[0]),
final: arr.map(e=>Number(e[1].finalLines.toFixed(2))),
added: arr.map(e=>e[1].rawAdded),
deleted: arr.map(e=>e[1].rawDeleted)
};
}
function aggregateMonthSeries(byDateComputed){
const bucket={};
for(const [date,v] of Object.entries(byDateComputed)){
const d=new Date(date); if(isNaN(d)) continue;
const key=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
bucket[key]=bucket[key]||{final:0, added:0, deleted:0};
bucket[key].final+=v.finalLines; bucket[key].added+=v.rawAdded; bucket[key].deleted+=v.rawDeleted;
}
const arr=Object.entries(bucket).sort((a,b)=> a[0].localeCompare(b[0]));
return { labels:arr.map(e=>e[0]), final:arr.map(e=>Number(e[1].final.toFixed(2))), added:arr.map(e=>e[1].added), deleted:arr.map(e=>e[1].deleted) };
}
// 展示
async function displayFinalResults(){
const modalBody=document.getElementById('stats-modal-body'); if(!modalBody) return;
const stored=JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEY_DATA)||'{}');
runtime.byDate=stored.byDate||{}; runtime.byDateSuffix=stored.byDateSuffix||{};
try{ await loadChartJs(); }catch(e){ console.warn(LOG_PREFIX,'Chart.js 加载失败',e); }
try{ await loadSheetJS(); }catch(e){ console.warn(LOG_PREFIX,'SheetJS 加载失败',e); }
const rates=loadRates();
const { byDateComputed, bySuffixComputed }=computeFinal(runtime.byDate, runtime.byDateSuffix, rates);
const dates=Object.keys(byDateComputed).sort((a,b)=> new Date(b)-new Date(a));
if(dates.length===0){ modalBody.innerHTML='<p>未找到有效提交记录。</p>'; return; }
let totalAdded=0,totalDeleted=0,totalCommits=0,totalFinal=0;
dates.forEach(d=>{ const v=byDateComputed[d]; totalAdded+=v.rawAdded; totalDeleted+=v.rawDeleted; totalCommits+=v.commitCount; totalFinal+=v.finalLines; });
// 工具条(中文按钮;无进度条/百分比)
const toolsHTML = `
<div class="cs-toolbar">
<span class="cs-chip">统计完成</span>
<span style="color:#888;">可导出或调整折算系数</span>
<span class="cs-divider"></span>
<div class="cs-inline" style="margin-left:auto;">
<button id="btn-export-day" class="cs-btn cs-btn-primary">导出Excel(按日)</button>
<button id="btn-export-suffix" class="cs-btn cs-btn-primary">导出Excel(按后缀)</button>
<button id="btn-export-trend-day" class="cs-btn">导出Excel(趋势-日)</button>
<button id="btn-export-trend-month" class="cs-btn">导出Excel(趋势-月)</button>
<button id="btn-toggle-rates" class="cs-btn">折算系数</button>
</div>
</div>
<div class="cs-flex" style="margin:6px 0;">
<strong>趋势视图:</strong>
<label class="radio-inline"><input type="radio" name="trend-mode" value="day" checked> 按天</label>
<label class="radio-inline"><input type="radio" name="trend-mode" value="month"> 按月</label>
<span class="cs-divider"></span>
<label class="checkbox-inline"><input type="checkbox" name="series" value="final" checked> 最终(折算)</label>
<label class="checkbox-inline"><input type="checkbox" name="series" value="added" checked> 原始新增</label>
<label class="checkbox-inline"><input type="checkbox" name="series" value="deleted" checked> 原始删除</label>
</div>
<div class="cs-section" style="height:320px;">
<canvas id="stats-trend-canvas"></canvas>
</div>
<div id="rates-panel" class="cs-section" style="display:none;">
<div class="cs-flex" style="justify-content:space-between;">
<strong>文件后缀折算系数(保存后即时重算)</strong>
<div class="cs-inline">
<button id="btn-rates-save" class="cs-btn cs-btn-primary">保存</button>
<button id="btn-rates-reset" class="cs-btn">恢复默认</button>
</div>
</div>
<div id="rates-table-wrapper" style="max-height:260px;overflow:auto;margin-top:8px;"></div>
</div>
`;
// 按日表
let daily = `<table class="table table-bordered table-striped" style="margin-top:6px;width:100%;">
<thead><tr><th>日期</th><th>提交次数</th><th>原始新增</th><th>原始删除</th><th style="background-color:#e8f5e9;">最终代码行</th></tr></thead><tbody>`;
dates.forEach(d=>{ const v=byDateComputed[d];
daily += `<tr><td>${d}</td><td>${v.commitCount}</td><td style="color:green;">+${v.rawAdded}</td><td style="color:red;">-${v.rawDeleted}</td><td style="font-weight:bold;background-color:#f1f8e9;">${v.finalLines.toFixed(2)}</td></tr>`;
});
daily += `<tr style="font-weight:bold;border-top:2px solid #1976d2;">
<td>总计</td><td>${totalCommits}</td><td style="color:green;">+${totalAdded}</td><td style="color:red;">-${totalDeleted}</td><td style="background-color:#dcedc8;">${totalFinal.toFixed(2)}</td></tr></tbody></table>`;
// 按后缀表
const suffixes=Object.keys(bySuffixComputed).sort();
let bySuffix = `<h4 style="margin-top:16px;border-bottom:1px solid #eee;padding-bottom:6px;">按文件类型汇总</h4>
<table class="table table-bordered table-striped" style="margin-top:6px;width:100%;">
<thead><tr><th>文件后缀</th><th>原始新增</th><th>原始删除</th><th style="background-color:#e8f5e9;">最终代码行</th></tr></thead><tbody>`;
suffixes.forEach(s=>{ const d=bySuffixComputed[s];
bySuffix += `<tr><td>${s}</td><td style="color:green;">+${d.rawAdded}</td><td style="color:red;">-${d.rawDeleted}</td><td style="font-weight:bold;background-color:#f1f8e9;">${d.finalLines.toFixed(2)}</td></tr>`;
});
bySuffix += `</tbody></table>`;
modalBody.innerHTML = toolsHTML + daily + bySuffix;
// 系数面板
renderRatesTable(rates);
document.getElementById('btn-toggle-rates').onclick=()=>{
const p=document.getElementById('rates-panel'); p.style.display=(p.style.display==='none'?'block':'none');
};
document.getElementById('btn-rates-save').onclick=()=>{
const inputs=document.querySelectorAll('.rate-input[data-suffix]'); const update={};
inputs.forEach(inp=>{ const suf=inp.dataset.suffix; const val=parseFloat(inp.value); if(!isNaN(val)) update[suf]=val; });
saveRates(update);
displayFinalResults(); // 直接重算并重绘
};
document.getElementById('btn-rates-reset').onclick=()=>{
localStorage.removeItem(RATES_STORE_KEY); renderRatesTable(loadRates()); displayFinalResults();
};
// 趋势图
const radios=modalBody.querySelectorAll('input[name="trend-mode"]');
const checks=modalBody.querySelectorAll('input[name="series"]');
const draw=(mode, show)=>{
const { byDateComputed }=computeFinal(runtime.byDate, runtime.byDateSuffix, loadRates());
const day=aggregateDaySeries(byDateComputed), month=aggregateMonthSeries(byDateComputed);
renderTrendChart(mode, mode==='month'?month:day, show);
};
radios.forEach(r=>r.addEventListener('change',()=>{ draw(getMode(), getVisible()); }));
checks.forEach(c=>c.addEventListener('change',()=>{ draw(getMode(), getVisible()); }));
const getMode=()=>document.querySelector('input[name="trend-mode"]:checked').value;
const getVisible=()=>{ const vis={final:false,added:false,deleted:false}; document.querySelectorAll('input[name="series"]').forEach(x=>vis[x.value]=x.checked); return vis; };
draw('day',{final:true,added:true,deleted:true});
// 导出
document.getElementById('btn-export-day').onclick=()=>{
const sorted=[...dates].sort((a,b)=> new Date(a)-new Date(b));
const rows=[['Date','Commits','RawAdded','RawDeleted','FinalLines']];
sorted.forEach(d=>{ const v=byDateComputed[d]; rows.push([d, v.commitCount, v.rawAdded, v.rawDeleted, Number(v.finalLines.toFixed(2))]); });
rows.push(['Total', totalCommits, totalAdded, totalDeleted, Number(totalFinal.toFixed(2))]);
exportExcel([{name:'Daily', rows}], 'CodeStats_Daily.xlsx');
};
document.getElementById('btn-export-suffix').onclick=()=>{
const rows=[['Suffix','RawAdded','RawDeleted','FinalLines']];
suffixes.forEach(s=>{ const d=bySuffixComputed[s]; rows.push([s, d.rawAdded, d.rawDeleted, Number(d.finalLines.toFixed(2))]); });
exportExcel([{name:'BySuffix', rows}], 'CodeStats_BySuffix.xlsx');
};
document.getElementById('btn-export-trend-day').onclick=()=>{
const series=aggregateDaySeries(byDateComputed);
const rows=[['Date','Final(Adjusted)','RawAdded','RawDeleted']];
series.labels.forEach((lbl,i)=> rows.push([lbl, series.final[i], series.added[i], series.deleted[i]]));
exportExcel([{name:'Trend_Day', rows}], 'CodeStats_TrendDay.xlsx');
};
document.getElementById('btn-export-trend-month').onclick=()=>{
const series=aggregateMonthSeries(byDateComputed);
const rows=[['Month','Final(Adjusted)','RawAdded','RawDeleted']];
series.labels.forEach((lbl,i)=> rows.push([lbl, series.final[i], series.added[i], series.deleted[i]]));
exportExcel([{name:'Trend_Month', rows}], 'CodeStats_TrendMonth.xlsx');
};
}
// 简洁的加载提示(去掉进度条/百分比)
function updateStatus(message){
const modalBody=document.getElementById('stats-modal-body');
if(!modalBody) return;
modalBody.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:24px 0;">
<div class="cs-spinner"></div>
<div style="color:#1976d2;font-weight:600;">${message}</div>
<div style="color:#999;">请保持页面不最小化,统计完成后会自动显示结果</div>
</div>
`;
}
// 趋势图渲染
function renderTrendChart(mode, pack, visible){
const canvas=document.getElementById('stats-trend-canvas'); if(!canvas || !window.Chart) return;
const ctx=canvas.getContext('2d');
const ds=[];
if(visible.final) ds.push({label: mode==='month'?'每月最终(折算)':'每天最终(折算)', data:pack.final, borderWidth:2, tension:.25, pointRadius:2});
if(visible.added) ds.push({label: mode==='month'?'每月原始新增' :'每天原始新增', data:pack.added, borderWidth:2, tension:.25, pointRadius:2});
if(visible.deleted) ds.push({label: mode==='month'?'每月原始删除' :'每天原始删除', data:pack.deleted, borderWidth:2, tension:.25, pointRadius:2});
if(chartInstance) chartInstance.destroy();
chartInstance = new Chart(ctx, {
type:'line',
data:{ labels:pack.labels, datasets:ds },
options:{
responsive:true, maintainAspectRatio:false,
plugins:{ legend:{ display:true } },
interaction:{ mode:'index', intersect:false },
scales:{ x:{ title:{ display:true, text: mode==='month'?'月份':'日期' } }, y:{ title:{ display:true, text:'代码行数' }, beginAtZero:true } }
}
});
}
// 系数面板
function renderRatesTable(rates){
const wrap=document.getElementById('rates-table-wrapper'); if(!wrap) return;
const keys=Object.keys(rates).sort();
let html=`<table class="table table-bordered table-striped" style="width:100%;">
<thead><tr><th>后缀</th><th>折算系数</th></tr></thead><tbody>`;
keys.forEach(k=>{
html += `<tr><td>${k}</td><td><input type="number" step="0.1" class="form-control rate-input" data-suffix="${k}" value="${rates[k]}"></td></tr>`;
});
html += `</tbody></table>
<small style="color:#888;">提示:html/jsp/htm/shtml 始终按 0.1 计算(固定规则)。</small>`;
wrap.innerHTML=html;
}
// 弹窗
function createModal(){
const modal=document.createElement('div');
modal.id='stats-modal-container';
modal.style.cssText='display:none;position:fixed;z-index:9999;left:0;top:0;width:100%;height:100%;overflow:auto;background:rgba(0,0,0,.6);align-items:center;justify-content:center;';
modal.innerHTML=`
<div id="stats-modal-content" style="background:#fff;margin:auto;padding:18px;border:1px solid #eaecef;border-radius:14px;width:86%;max-width:1000px;box-shadow:0 6px 18px rgba(0,0,0,.08);animation:fadeIn .25s;">
<div id="stats-modal-header" style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f0f0f0;padding-bottom:8px;margin-bottom:10px;">
<h2 style="margin:0;font-size:18px;">代码量统计结果</h2>
<span id="stats-modal-close" style="color:#999;font-size:26px;font-weight:bold;cursor:pointer;">×</span>
</div>
<div id="stats-modal-body" style="max-height:66vh;overflow-y:auto;"></div>
</div>
`;
document.body.appendChild(modal);
const styleSheet=document.createElement('style');
styleSheet.type='text/css';
styleSheet.innerText=`@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}`;
document.head.appendChild(styleSheet);
document.getElementById('stats-modal-close').onclick=()=>{ modal.style.display='none'; };
modal.onclick=(e)=>{ if(e.target===modal){ modal.style.display='none'; } };
}
// 工具
function getPagerInfo(){
const pager=document.querySelector(PAGER_CONTAINER_SELECTOR);
if(!pager) return {currentPage:1,totalPages:1};
const total=parseInt(pager.dataset.recTotal||'0',10),
per=parseInt(pager.dataset.recPerPage||'100',10),
page=parseInt(pager.dataset.page||'1',10);
const pages=Math.ceil(total/per);
return { currentPage:page, totalPages: pages>0?pages:1 };
}
function createLink(module, method, params){
const url=new URL(window.location.href);
url.searchParams.set('m', module); url.searchParams.set('f', method);
if(params){ params.split('&').forEach(p=>{ const [k,v]=p.split('='); url.searchParams.set(k,v); }); }
return url.toString();
}
// 入口
(function(){
const onReady=()=>{
createUI();
if(sessionStorage.getItem(SESSION_STORAGE_KEY_FLAG)==='true'){
document.getElementById('stats-modal-container').style.display='flex';
isProcessing=true;
const statsButton=document.getElementById('start-stats-button');
if(statsButton){ statsButton.textContent='统计中…'; statsButton.disabled=true; }
processCurrentPage();
}
};
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', onReady); } else { onReady(); }
})();
})();