logo NodeSeekbeta

[nodeseek年度报告]鸡腿统计插件

闲着没事干调教ai写个油猴脚本,复制到浏览器里运行即可,专门统计鸡腿获取日志,在鸡腿日志页面点击运行https://www.nodeseek.com/credit#/p-1

676ad0a00acd6.png

附件

// ==UserScript==
// @name         NodeSeek鸡腿变化趋势图
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description  在NodeSeek论坛显示鸡腿数量随时间变化的趋势图,饼图和正态分布图,使用ECharts自渲染图表
// @author       Your name
// @match        https://www.nodeseek.com/credit*
// @grant        GM_xmlhttpRequest
// @grant        GM_cookie
// @connect      nodeseek.com
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js
// ==/UserScript==

(function() {
    'use strict';

    // 配置项
    const CONFIG = {
        FETCH_DELAY: 1000,    // 每次请求间隔(毫秒)
        MAX_PAGES: 10,        // 最大获取页数
        CHART_HEIGHT: 300,    // 减小图表高度到300px
        CHART_WIDTH: '100%',  // 图表宽度(自适应)
        ANIMATION_DURATION: 1000, // 动画持续时间
    };

    // 样式定义
    const STYLES = {
        container: {
            padding: '20px',
            backgroundColor: '#f8f9fa',
            borderRadius: '8px',
            marginBottom: '20px'
        },
        button: {
            padding: '8px 16px',
            backgroundColor: '#40a9ff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            marginBottom: '10px',
            fontSize: '14px'
        },
        buttonHover: {
            backgroundColor: '#1890ff'
        },
        buttonDisabled: {
            backgroundColor: '#bfbfbf',
            cursor: 'not-allowed'
        },
        progress: {
            width: '100%',
            height: '6px',
            backgroundColor: '#e9ecef',
            borderRadius: '3px',
            overflow: 'hidden',
            marginBottom: '10px'
        },
        progressBar: {
            height: '100%',
            backgroundColor: '#40a9ff',
            transition: 'width 0.3s ease'
        },
        status: {
            fontSize: '14px',
            color: '#666',
            marginBottom: '10px'
        },
        chartContainer: {
            width: '100%',
            height: `${CONFIG.CHART_HEIGHT}px`,
            margin: '10px 0',
            backgroundColor: '#fff',
            borderRadius: '8px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
            display: 'none'  // 初始隐藏图表容器
        }
    };

    // 数据处理类
    class CreditDataProcessor {
        constructor() {
            this.creditData = [];
            this.currentPage = 1;
            this.totalPages = 1;
            this.reasonStats = {
                '回帖奖励': 0,
                '发帖奖励': 0,
                '签到': 0,
                '其他': 0
            };
            this.signInData = new Map(); // 使用Map存储每天的签到数据
        }

        // 解析HTML内容
        parseHtml(html) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const rows = doc.querySelectorAll('tr[data-v-e2af6d06]');
            console.log(`解析第${this.currentPage}页,找到数据行数:`, rows.length);

            rows.forEach(row => {
                const cells = row.querySelectorAll('td[data-v-e2af6d06]');
                if (cells.length >= 4) {
                    const changeAmount = parseInt(cells[0].textContent.trim()) || 0;
                    const totalAmount = parseInt(cells[1].textContent.trim()) || 0;
                    const reason = cells[2].textContent.trim(); // 获取理由
                    const timeText = cells[3].textContent.trim();
                    const timestamp = new Date(timeText).getTime();

                    if (!isNaN(timestamp) && !isNaN(totalAmount)) {
                        this.creditData.push({
                            time: timestamp,
                            value: totalAmount,
                            change: changeAmount,
                            reason: reason,
                            date: new Date(timestamp)
                        });
                    }
                }
            });

            // 检查是否有下一页
            const pagerButtons = doc.querySelectorAll('button[data-v-e2af6d06]');
            const pageNumbers = Array.from(pagerButtons)
                .map(btn => parseInt(btn.textContent.trim()))
                .filter(num => !isNaN(num));

            if (pageNumbers.length > 0) {
                this.totalPages = Math.max(...pageNumbers);
            }
        }

        // 聚合数据
        aggregateData(data, interval) {
            const aggregated = new Map();

            data.forEach(item => {
                let key;
                const date = new Date(item.time);

                switch(interval) {
                    case 'day':
                        key = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
                        break;
                    case 'week':
                        const day = date.getDay();
                        const diff = date.getDate() - day + (day === 0 ? -6 : 1);
                        key = new Date(date.getFullYear(), date.getMonth(), diff).getTime();
                        break;
                    case 'month':
                        key = new Date(date.getFullYear(), date.getMonth(), 1).getTime();
                        break;
                }

                if (!aggregated.has(key)) {
                    aggregated.set(key, {
                        time: key,
                        value: item.value,
                        count: 1,
                        change: item.change || 0 // 添加change字段
                    });
                } else {
                    const existing = aggregated.get(key);
                    if (item.value > existing.value) {
                        existing.value = item.value;
                    }
                    existing.count++;
                    if (item.change) {
                        existing.change = item.change; // 使用当天的变化值
                    }
                }
            });

            return Array.from(aggregated.values())
                .sort((a, b) => a.time - b.time);
        }

        // 计算正态分布数据
        calculateNormalDistribution(data) {
            // 计算平均值
            const sum = data.reduce((acc, val) => acc + val, 0);
            const mean = sum / data.length;

            // 计算标准差
            const squareDiffs = data.map(value => {
                const diff = value - mean;
                return diff * diff;
            });
            const variance = squareDiffs.reduce((acc, val) => acc + val, 0) / data.length;
            const standardDeviation = Math.sqrt(variance);

            // 统计每个值的出现次数
            const valueCounts = new Map();
            data.forEach(value => {
                valueCounts.set(value, (valueCounts.get(value) || 0) + 1);
            });

            // 生成分布数据
            const distributionData = Array.from(valueCounts.entries())
                .map(([value, count]) => ({
                    value: value,
                    count: count,
                    percentage: (count / data.length) * 100
                }))
                .sort((a, b) => a.value - b.value);

            return {
                distribution: distributionData,
                mean: mean,
                standardDeviation: standardDeviation,
                totalSamples: data.length
            };
        }

        // 处理数据,生成图表所需格式
        processDataForChart() {
            // 按时间排序
            this.creditData.sort((a, b) => a.time - b.time);

            // 统计不同类型的数据
            this.creditData.forEach(item => {
                const reason = item.reason?.trim() || "";
                if (reason.includes('回帖')) {
                    this.reasonStats['回帖奖励'] += item.change;
                } else if (reason.includes('发帖')) {
                    this.reasonStats['发帖奖励'] += item.change;
                } else if (reason.includes('签到')) {
                    this.reasonStats['签到'] += item.change;
                    // 记录签到数据
                    const match = reason.match(/\d+/);
                    if (match) {
                        const signInAmount = parseInt(match[0]);
                        const dateKey = new Date(item.time).setHours(0, 0, 0, 0);
                        this.signInData.set(dateKey, signInAmount);
                    }
                } else {
                    this.reasonStats['其他'] += item.change;
                }
            });

            // 生成日期格式化函数
            const formatDate = (timestamp) => {
                const date = new Date(timestamp);
                return `${date.getMonth() + 1}/${date.getDate()}`;
            };

            // 生成每日变化数据
            const dailyData = this.aggregateData(this.creditData, 'day');

            // 生成签到数据数组
            const signInValues = Array.from(this.signInData.values());
            const normalDistribution = this.calculateNormalDistribution(signInValues);

            // 生成饼图数据
            const pieData = Object.entries(this.reasonStats).map(([name, value]) => ({
                name,
                value
            }));

            return {
                daily: {
                    times: dailyData.map(item => formatDate(item.time)),
                    values: dailyData.map(item => item.value)
                },
                signInStats: normalDistribution,
                pie: pieData
            };
        }
    }

    // 图表类
    class CreditChart {
        constructor(container) {
            this.trendChart = echarts.init(container.trend);
            this.pieChart = echarts.init(container.pie);
            this.distributionChart = echarts.init(container.distribution);

            // 添加窗口大小变化监听
            window.addEventListener('resize', () => {
                this.trendChart.resize();
                this.pieChart.resize();
                this.distributionChart.resize();
            });
        }

        // 渲染图表
        render(data, instant = false) {
            // 显示图表容器
            this.trendChart.getDom().style.display = 'block';
            this.pieChart.getDom().style.display = 'block';
            this.distributionChart.getDom().style.display = 'block';

            // 配置趋势图
            const trendOption = {
                title: {
                    text: '鸡腿变化趋势图',
                    left: 'center',
                    textStyle: {
                        fontSize: 18,
                        color: '#333'
                    }
                },
                tooltip: {
                    trigger: 'axis',
                    backgroundColor: 'rgba(255, 255, 255, 0.9)',
                    borderColor: '#ccc',
                    borderWidth: 1,
                    textStyle: {
                        color: '#333'
                    }
                },
                xAxis: {
                    type: 'category',
                    data: data.daily.times,
                    axisLabel: {
                        rotate: 45,
                        interval: 'auto',
                        fontSize: 10,
                        color: '#666'
                    },
                    axisLine: {
                        lineStyle: {
                            color: '#ddd'
                        }
                    }
                },
                yAxis: {
                    type: 'value',
                    name: '鸡腿总量',
                    nameTextStyle: {
                        color: '#666'
                    },
                    axisLine: {
                        show: true,
                        lineStyle: {
                            color: '#ddd'
                        }
                    },
                    splitLine: {
                        lineStyle: {
                            color: '#eee'
                        }
                    }
                },
                series: [{
                    name: '鸡腿总量',
                    type: 'line',
                    data: data.daily.values,
                    smooth: true,
                    showSymbol: false,
                    lineStyle: {
                        width: 2,
                        color: '#40a9ff'
                    },
                    areaStyle: {
                        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
                            offset: 0,
                            color: 'rgba(64,169,255,0.3)'
                        }, {
                            offset: 1,
                            color: 'rgba(64,169,255,0.05)'
                        }])
                    }
                }]
            };

            // 配置饼图
            const pieOption = {
                title: {
                    text: '鸡腿获取来源分布',
                    left: 'center',
                    textStyle: {
                        fontSize: 18,
                        color: '#333'
                    }
                },
                tooltip: {
                    trigger: 'item',
                    formatter: '{b}: {d}%'
                },
                legend: {
                    orient: 'horizontal',
                    bottom: '0%',
                    data: data.pie.map(item => item.name),
                    textStyle: {
                        fontSize: 12,
                        color: '#666'
                    }
                },
                series: [{
                    name: '来源',
                    type: 'pie',
                    radius: ['40%', '60%'],
                    avoidLabelOverlap: false,
                    label: {
                        show: false,
                        position: 'center'
                    },
                    emphasis: {
                        label: {
                            show: true,
                            fontSize: '14',
                            fontWeight: 'bold'
                        }
                    },
                    labelLine: {
                        show: false
                    },
                    data: data.pie
                }]
            };

            // 配置正态分布图
            const distributionOption = {
                title: {
                    text: '签到鸡腿分布图',
                    subtext: [
                        `平均值: ${data.signInStats.mean.toFixed(1)}`,
                        `标准差: ${data.signInStats.standardDeviation.toFixed(1)}`,
                        `样本数: ${data.signInStats.totalSamples}`
                    ].join('\n'),
                    left: 'center',
                    textStyle: {
                        fontSize: 18,
                        color: '#333'
                    },
                    subtextStyle: {
                        color: '#666',
                        fontSize: 12,
                        lineHeight: 18
                    }
                },
                tooltip: {
                    trigger: 'axis',
                    axisPointer: {
                        type: 'shadow'
                    },
                    formatter: function(params) {
                        const item = params[0];
                        return `获得${item.name}个鸡腿<br>出现次数:${item.value}<br>占比:${item.data.percentage.toFixed(1)}%`;
                    }
                },
                grid: {
                    top: '25%',  // 为标题和副标题留出更多空间
                    left: '10%',
                    right: '10%',
                    bottom: '15%'
                },
                xAxis: {
                    type: 'category',
                    data: data.signInStats.distribution.map(item => item.value),
                    name: '获得鸡腿数',
                    nameTextStyle: {
                        color: '#666',
                        fontSize: 12,
                        padding: [10, 0, 0, 0]
                    },
                    axisLabel: {
                        fontSize: 10,
                        color: '#666'
                    },
                    axisLine: {
                        lineStyle: {
                            color: '#ddd'
                        }
                    }
                },
                yAxis: {
                    type: 'value',
                    name: '出现次数',
                    nameTextStyle: {
                        color: '#666',
                        fontSize: 12,
                        padding: [0, 0, 0, 30]
                    },
                    axisLine: {
                        show: true,
                        lineStyle: {
                            color: '#ddd'
                        }
                    },
                    splitLine: {
                        lineStyle: {
                            color: '#eee'
                        }
                    }
                },
                series: [{
                    name: '出现次数',
                    type: 'bar',
                    data: data.signInStats.distribution.map(item => ({
                        value: item.count,
                        percentage: item.percentage
                    })),
                    itemStyle: {
                        color: '#73d13d'
                    },
                    emphasis: {
                        itemStyle: {
                            color: '#95de64'
                        }
                    },
                    label: {
                        show: true,
                        position: 'top',
                        formatter: '{c}',
                        fontSize: 10,
                        color: '#666'
                    }
                }]
            };

            // 设置图表配置
            this.trendChart.setOption(trendOption);
            this.pieChart.setOption(pieOption);
            this.distributionChart.setOption(distributionOption);

            // 强制触发一次resize以确保图表正确渲染
            this.trendChart.resize();
            this.pieChart.resize();
            this.distributionChart.resize();
        }

        // 清除图表
        clear() {
            this.trendChart.clear();
            this.pieChart.clear();
            this.distributionChart.clear();

            // 隐藏图表容器
            this.trendChart.getDom().style.display = 'none';
            this.pieChart.getDom().style.display = 'none';
            this.distributionChart.getDom().style.display = 'none';
        }
    }

    // UI控制器类
    class UIController {
        constructor() {
            this.container = null;
            this.button = null;
            this.progressBar = null;
            this.statusText = null;
            this.chartContainers = null;
        }

        // 创建UI元素
        createUI() {
            // 创建主容器
            this.container = document.createElement('div');
            Object.assign(this.container.style, STYLES.container);

            // 创建按钮
            this.button = document.createElement('button');
            Object.assign(this.button.style, STYLES.button);
            this.button.textContent = '开始统计鸡腿变化';
            this.button.addEventListener('mouseover', () => {
                if (!this.button.disabled) {
                    Object.assign(this.button.style, STYLES.buttonHover);
                }
            });
            this.button.addEventListener('mouseout', () => {
                if (!this.button.disabled) {
                    Object.assign(this.button.style, STYLES.button);
                }
            });

            // 创建进度条容器
            const progressContainer = document.createElement('div');
            Object.assign(progressContainer.style, STYLES.progress);

            // 创建进度条
            this.progressBar = document.createElement('div');
            Object.assign(this.progressBar.style, STYLES.progressBar);
            this.progressBar.style.width = '0%';
            progressContainer.appendChild(this.progressBar);

            // 创建状态文本
            this.statusText = document.createElement('div');
            Object.assign(this.statusText.style, STYLES.status);
            this.statusText.textContent = '点击按钮开始统计';

            // 创建图表容器
            this.chartContainers = {
                trend: document.createElement('div'),
                pie: document.createElement('div'),
                distribution: document.createElement('div')
            };

            // 创建图表容器的包装器
            const chartsWrapper = document.createElement('div');
            chartsWrapper.style.marginTop = '20px';

            Object.keys(this.chartContainers).forEach(key => {
                const chartDiv = this.chartContainers[key];
                Object.assign(chartDiv.style, STYLES.chartContainer);
                chartDiv.id = `credit-chart-${key}`;
                chartsWrapper.appendChild(chartDiv);
            });

            // 组装UI
            this.container.insertBefore(this.button, this.container.firstChild);
            this.container.insertBefore(this.statusText, this.button.nextSibling);
            this.container.insertBefore(progressContainer, this.statusText.nextSibling);
            this.container.appendChild(chartsWrapper);

            // 插入到页面
            const targetElement = document.querySelector('.credit-table');
            if (targetElement) {
                targetElement.parentNode.insertBefore(this.container, targetElement);
            }

            return this.chartContainers;
        }

        // 更新进度
        updateProgress(current, total) {
            const percentage = (current / total) * 100;
            this.progressBar.style.width = `${percentage}%`;
            this.statusText.textContent = `正在获取数据... ${current}/${total} 页`;
        }

        // 更新状态
        updateStatus(status) {
            this.statusText.textContent = status;
        }

        // 禁用按钮
        disableButton() {
            this.button.disabled = true;
            Object.assign(this.button.style, STYLES.buttonDisabled);
            this.button.textContent = '正在统计中...';
        }

        // 启用按钮
        enableButton() {
            this.button.disabled = false;
            Object.assign(this.button.style, STYLES.button);
            this.button.textContent = '开始统计鸡腿变化';
        }
    }

    // 控制器
    class CreditController {
        constructor() {
            this.processor = new CreditDataProcessor();
            this.ui = new UIController();
            this.chartContainer = this.ui.createUI();
            this.chart = new CreditChart(this.chartContainer);
            this.bindEvents();
            this.currentUrl = new URL(window.location.href);
        }

        // 绑定事件
        bindEvents() {
            this.ui.button.addEventListener('click', () => {
                this.startProcessing();
            });
        }

        // 更新URL并触发页面加载
        async navigateToPage(page) {
            // 更新URL的页码
            this.currentUrl.hash = `/p-${page}`;
            window.location.hash = `/p-${page}`;

            // 等待页面数据加载
            await new Promise(resolve => setTimeout(resolve, CONFIG.FETCH_DELAY));

            // 等待表格元素出现
            let retries = 0;
            while (retries < 5) {
                const rows = document.querySelectorAll('tr[data-v-e2af6d06]');
                if (rows.length > 0) {
                    return true;
                }
                await new Promise(resolve => setTimeout(resolve, 500));
                retries++;
            }
            return false;
        }

        // 获取当前页面数据
        parseCurrentPage() {
            const rows = document.querySelectorAll('tr[data-v-e2af6d06]');
            const data = [];

            rows.forEach(row => {
                const cells = row.querySelectorAll('td[data-v-e2af6d06]');
                if (cells.length >= 4) {
                    const changeAmount = parseInt(cells[0].textContent.trim()) || 0;
                    const totalAmount = parseInt(cells[1].textContent.trim()) || 0;
                    const reason = cells[2].textContent.trim(); // 获取理由
                    const timeText = cells[3].textContent.trim();
                    const timestamp = new Date(timeText).getTime();

                    if (!isNaN(timestamp) && !isNaN(totalAmount)) {
                        data.push({
                            time: timestamp,
                            value: totalAmount,
                            change: changeAmount,
                            reason: reason,
                            date: new Date(timestamp)
                        });
                    }
                }
            });

            return data;
        }

        // 获取最大页数
        getMaxPages() {
            const pagerLinks = document.querySelectorAll('a[class*="pager-pos"]');
            let maxPage = 1;

            pagerLinks.forEach(link => {
                const href = link.getAttribute('href');
                if (href) {
                    const match = href.match(/\/p-(\d+)/);
                    if (match) {
                        const pageNum = parseInt(match[1]);
                        if (!isNaN(pageNum) && pageNum > maxPage) {
                            maxPage = pageNum;
                        }
                    }
                }
            });

            console.log('检测到最大页数:', maxPage);
            return maxPage;
        }

        // 开始处理数据
        async startProcessing() {
            this.ui.disableButton();
            this.chart.clear();
            this.processor = new CreditDataProcessor();

            try {
                // 获取实际的最大页数
                const totalPages = this.getMaxPages();
                if (totalPages < 1) {
                    throw new Error('无法获取总页数');
                }
                console.log('开始处理,总页数:', totalPages);

                let currentPage = 1;
                let successCount = 0;
                let failCount = 0;

                // 处理所有页面
                while (currentPage <= totalPages) {
                    this.ui.updateProgress(currentPage, totalPages);
                    this.ui.updateStatus(`正在获取第${currentPage}页数据...(成功:${successCount},失败:${failCount})`);

                    // 导航到指定页面
                    if (await this.navigateToPage(currentPage)) {
                        const currentData = this.parseCurrentPage();
                        if (currentData.length > 0) {
                            this.processor.creditData.push(...currentData);
                            console.log(`已处理第${currentPage}页,获取到${currentData.length}条数据`);
                            successCount++;
                        } else {
                            console.warn(`第${currentPage}页未获取到数据`);
                            failCount++;
                        }
                    } else {
                        console.error(`无法加载第${currentPage}页数据`);
                        failCount++;
                    }

                    currentPage++;
                    // 添加延迟避免过快请求
                    await new Promise(resolve => setTimeout(resolve, 500));
                }

                if (this.processor.creditData.length === 0) {
                    throw new Error('未能获取到任何数据');
                }

                const chartData = this.processor.processDataForChart();
                this.ui.updateStatus('数据解析完成,正在绘制图表...');

                // 渲染图表
                this.chart.render(chartData);

                this.ui.updateStatus(`统计完成!共处理${this.processor.creditData.length}条数据(成功:${successCount}页,失败:${failCount}页)`);

                // 恢复第一页
                await this.navigateToPage(1);
            } catch (error) {
                console.error('处理数据失败:', error);
                this.ui.updateStatus(`处理数据失败:${error.message}`);
            } finally {
                this.ui.enableButton();
            }
        }
    }

    // 初始化
    function init() {
        // 检查是否在鸡腿页面
        if (window.location.pathname.includes('/credit')) {
            console.log('初始化鸡腿统计图表...');
            // 等待Vue.js渲染完成
            setTimeout(() => {
                new CreditController();
            }, 1000);
        } else {
            console.log('不在鸡腿页面,当前路径:', window.location.pathname);
        }
    }

    // 等待页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
1234
  • @kurssy #17 @KefIe #15

    其实咱早就搞了,没想到被你们这些“穿越者”偷跑了,等官方发布吧

  • 绑定

  • 太牛了

  • nb

  • @KefIe #2 你居然做过了 xhj005 ,想法不约而同了,感觉可以把功能整合一下

  • 要是有个链接点击就能看到就好了 xhj001

  • 最后应该计算下平均数,看是不是约等于5

  • @shuai #6 好早有这想法就是鸡腿页面有个类似githubstar增长数的那种图

  • image

  • @shuai #6 要不管理教下如果过cf盾 xhj010

1234

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有61578位seeker

🎉欢迎新用户🎉