算了么积分统计
// ==UserScript==
// @name 算了么积分统计
// @namespace http://tampermonkey.net/
// @version 0.2
// @description 在算了么网站积分页面插入积分分布饼图
// @author Kevin
// @match https://web.suanleme.cn/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @connect openapi.suanleme.cn
// @require https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js
// @require https://cdn.jsdelivr.net/npm/jsrsasign@10.9.0/lib/jsrsasign-all-min.js
// @require https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js
// @require https://cdn.jsdelivr.net/npm/jsencrypt/bin/jsencrypt.min.js
// @require https://cdn.jsdelivr.net/npm/dayjs@1.11.10/dayjs.min.js
// @require https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/utc.js
// @require https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/timezone.js
// ==/UserScript==
(function(){"use strict";dayjs.extend(window.dayjs_plugin_utc);dayjs.extend(window.dayjs_plugin_timezone);dayjs.tz.setDefault("Asia/Shanghai");const CONFIG={enableChart:GM_getValue("enableChart",true)};function registerMenuCommand(){GM_registerMenuCommand(`${CONFIG.enableChart?"✅":"❌"} 已${CONFIG.enableChart?"启用":"禁用"}积分图表`,()=>{CONFIG.enableChart=!CONFIG.enableChart;GM_setValue("enableChart",CONFIG.enableChart);location.reload()})}registerMenuCommand();const style=document.createElement("style");style.textContent=`
@keyframes spinner-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.custom-loading-container {
text-align: center;
margin-top: 80px;
}
.custom-spinner {
display: inline-block;
width: 24px;
height: 24px;
border: 2px solid rgba(0, 0, 0, 0.1);
border-left-color: #409eff;
border-radius: 50%;
animation: spinner-rotate 1s linear infinite;
margin-bottom: 10px;
}
.custom-loading-text {
font-size: 14px;
color: #606266;
}
`;document.head.appendChild(style);const REQUEST_DELAY_MS=1e3;function waitForElement(selector,callback,maxTries=100){if(maxTries<=0)return;const element=document.querySelector(selector);if(element){callback(element)}else{setTimeout(()=>{waitForElement(selector,callback,maxTries-1)},100)}}const{token:userToken,rsa_prik:rsaPrik}=JSON.parse(localStorage.getItem("userToken"));const jsRsa=new JSEncrypt;function generateSignStr(headers,url2){jsRsa.setPrivateKey(rsaPrik);const dataStr="";return jsRsa.sign(`/api${url2}\n${headers["version"]}\n${headers["timestamp"]}\n${headers["token"]}\n${dataStr}`,CryptoJS.SHA256,"sha256")}let abortController=null;async function getTodayScoreRecords(page=1,allRecords=[],updateLoadingText=null,retryCount=0){const MAX_RETRIES=10;if(updateLoadingText){const retryText=retryCount>0?`(重试第${retryCount}次)`:"";updateLoadingText(`正在获取今日数据(第${page}页)${retryText}`)}if(page===1){if(abortController){abortController.abort()}abortController=new AbortController}return new Promise((resolve,reject)=>{const today=dayjs().tz("Asia/Shanghai").format("YYYY-MM-DD");const headers={Accept:"*/*",version:"0.0.1",timestamp:Date.now(),token:userToken};const url="/user/score_record/list";headers["sign_str"]=generateSignStr(headers,url);headers["host"]="openapi.suanleme.cn";const timeoutId=setTimeout(()=>{if(!window.location.hash.includes("#/score")){console.log("【算了么积分统计】已离开积分页面,取消请求");reject(new Error("已离开积分页面"));return}GM_xmlhttpRequest({method:"GET",url:`https://openapi.suanleme.cn/api${url}?page=${page}&score_types=Recharge,OfflineRecharge,Withdrawal,ReceivingOrders,PublishTask,TaskReturn,EncounterReceivingOrders,MountingFee,CoverMountingFee,CdnFee,StorageFee,TrafficFee,BuyGoods`,headers:headers,onload:function(response){try{const data=JSON.parse(response.responseText);if(data.code==="0000"){const todayRecords=data.data.results.filter(record=>{const beijingDate=dayjs(record.created_time).tz("Asia/Shanghai");const recordDateStr=beijingDate.format("YYYY-MM-DD");return recordDateStr.startsWith(today)});allRecords.push(...todayRecords);if(todayRecords.length>0){console.log(`【算了么积分统计】获取第${page+1}页数据`);getTodayScoreRecords(page+1,allRecords,updateLoadingText).then(resolve).catch(reject)}else{resolve(allRecords)}}else{handleError(new Error(data.message))}}catch(error){handleError(error)}},onerror:handleError})},page>1?REQUEST_DELAY_MS:0);if(abortController){abortController.signal.addEventListener("abort",()=>{clearTimeout(timeoutId)})}function handleError(error){console.error("请求失败:",error);if(retryCount<MAX_RETRIES){console.log(`第${retryCount+1}次重试...`);setTimeout(()=>{getTodayScoreRecords(page,allRecords,updateLoadingText,retryCount+1).then(resolve).catch(reject)},1e3)}else{reject(new Error("获取数据失败,请刷新页面重试"))}}})}const scoreTypeMap={Recharge:"充值",OfflineRecharge:"线下充值",Withdrawal:"提现",ReceivingOrders:"接单",PublishTask:"发布任务",TaskReturn:"任务退回",EncounterReceivingOrders:"被接单",MountingFee:"挂载费用",CoverMountingFee:"被挂载费用",CdnFee:"CDN费用",StorageFee:"仓储费",TrafficFee:"流量费",BuyGoods:"购买商品"};function createChart(container,data){const chart=echarts.init(container);const scoreByType=data.reduce((acc,record)=>{acc[record.type]=(acc[record.type]||0)+record.score/1e3;return acc},{});const chartData=Object.entries(scoreByType).map(([type,score])=>({name:scoreTypeMap[type]||type,value:score.toFixed(3)}));const option={tooltip:{trigger:"item",formatter:function(params){return params.name+": "+params.value+" 积分 ("+params.percent.toFixed(3)+"%)"}},legend:{orient:"vertical",left:"left"},series:[{name:"积分分布",type:"pie",radius:"50%",data:chartData,emphasis:{itemStyle:{shadowBlur:10,shadowOffsetX:0,shadowColor:"rgba(0, 0, 0, 0.5)"}}}]};chart.setOption(option);return chart}function init(){if(!window.location.hash.includes("#/score"))return;if(!CONFIG.enableChart)return;const existingCharts=document.querySelectorAll(".content-item.block-item.detailed-box .header-title");const pieCharts=Array.from(existingCharts).filter(chart=>chart.textContent==="积分分布饼图");if(pieCharts.length>0){return}waitForElement(".el-radio-button",tabElement=>{const tabs=document.querySelectorAll(".el-radio-button");const detailTab=Array.from(tabs).find(tab=>tab.textContent.includes("明细"));if(detailTab){detailTab.click();waitForElement(".content-item.block-item.detailed-box",async detailedBox=>{const chartContainer=document.createElement("div");chartContainer.style.width="100%";chartContainer.style.height="100px";const newDetailedBox=document.createElement("div");newDetailedBox.className="content-item block-item detailed-box";const loadingContainer=document.createElement("div");loadingContainer.className="custom-loading-container";const loadingSpinner=document.createElement("div");loadingSpinner.className="custom-spinner";const loadingText=document.createElement("div");loadingText.className="custom-loading-text";loadingText.textContent="加载中";const boxHeader=document.createElement("div");boxHeader.className="box-header";const headerTitle=document.createElement("div");headerTitle.className="header-title";headerTitle.textContent="积分分布饼图";boxHeader.appendChild(headerTitle);loadingContainer.appendChild(loadingSpinner);loadingContainer.appendChild(loadingText);chartContainer.appendChild(loadingContainer);newDetailedBox.appendChild(boxHeader);newDetailedBox.appendChild(chartContainer);const detailedBoxes=document.querySelectorAll(".content-item.block-item.detailed-box");if(detailedBoxes.length>=2){detailedBoxes[0].parentNode.insertBefore(newDetailedBox,detailedBoxes[1]);try{const records=await getTodayScoreRecords(1,[],text=>{loadingText.textContent=text});if(records.length>0){chartContainer.style.height="400px";createChart(chartContainer,records);if(chartContainer.contains(loadingContainer)){chartContainer.removeChild(loadingContainer)}}else{loadingText.textContent="今日暂无积分记录";loadingSpinner.style.display="none"}}catch(error){console.error("获取积分记录失败:",error);loadingText.textContent="获取数据失败,请刷新页面重试";loadingSpinner.style.display="none"}}})}})}window.addEventListener("hashchange",event=>{if(!window.location.hash.includes("#/score")&&abortController){console.log("【算了么积分统计】离开积分页面,取消所有请求");abortController.abort();abortController=null;chartInitialized=false}init()});function throttle(func,delay){let lastCall=0;return function(...args){const now=Date.now();if(now-lastCall>=delay){lastCall=now;return func.apply(this,args)}}}let chartInitialized=false;let currentPageUrl=window.location.hash;const throttledInit=throttle(()=>{const newUrl=window.location.hash;const urlChanged=newUrl!==currentPageUrl;currentPageUrl=newUrl;if(!window.location.hash.includes("#/score")||!CONFIG.enableChart){if(abortController){console.log("【算了么积分统计】离开积分页面,取消所有请求");abortController.abort();abortController=null}chartInitialized=false;return}if(window.location.hash.includes("#/score")){const existingCharts=document.querySelectorAll(".content-item.block-item.detailed-box .header-title");const pieCharts=Array.from(existingCharts).filter(chart=>chart.textContent==="积分分布饼图");if(pieCharts.length>0){chartInitialized=true;if(pieCharts.length>1){console.log(`【算了么积分统计】检测到${pieCharts.length}个图表,移除多余的图表`);for(let i=1;i<pieCharts.length;i++){const chartBox=pieCharts[i].closest(".content-item.block-item.detailed-box");if(chartBox&&chartBox.parentNode){chartBox.parentNode.removeChild(chartBox)}}}if(urlChanged&&window.location.hash.includes("#/score")){console.log("【算了么积分统计】检测到URL变化,重新初始化图表");if(abortController){abortController.abort();abortController=null}chartInitialized=false;setTimeout(()=>{if(window.location.hash.includes("#/score")){init()}},300)}return}if(!chartInitialized){console.log("【算了么积分统计】检测到DOM变化,尝试初始化图表");chartInitialized=true;init()}}},300);const observer=new MutationObserver(()=>{throttledInit()});const config={childList:true,subtree:true,attributes:false,characterData:false};observer.observe(document.body,config);setTimeout(init,0)})();