您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
快捷月份、跨页统计、趋势图(多数据集)、导出Excel(.xlsx)、后缀折算系数可编辑并保存;中文UI与工具条美化;去掉进度条与100%显示。
// ==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(); } })(); })();