Greasy Fork

Greasy Fork is available in English.

雷速体育亚盘统计

在雷速移动端网页加入多场比赛亚盘统计功能,点击“统计”后,在每个比分下可以选择特定比赛进行让球亚盘统计,点击“显示”将加载数据并显示统计结果表格。

当前为 2025-04-03 提交的版本,查看 最新版本

// ==UserScript==
// @name         雷速体育亚盘统计
// @namespace    http://dol.freevar.com/
// @version      0.81
// @description  在雷速移动端网页加入多场比赛亚盘统计功能,点击“统计”后,在每个比分下可以选择特定比赛进行让球亚盘统计,点击“显示”将加载数据并显示统计结果表格。
// @author       Dolphin
// @match        https://m.leisu.com/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      pay.jcyqr.com
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 样式定义
    GM_addStyle(`
    input[type="checkbox"] {
    -webkit-appearance: auto;
    -moz-appearance: auto;
    appearance: auto;
    width: 4vw;
    height: 4vw;
    border: initial;
    background: initial;
    }
        .stats-table { border-collapse:collapse; margin:auto;}
        .stats-table tr:nth-child(odd) {background:#fff;}
        .stats-table td, .stats-table th { border:1px solid #ccf; text-align:center; padding:0 1vw; }
        .highlight-green { background:#cfc }
        .highlight-red { background:#fcc }
        #analysisButtons button {background:#ccf; padding:1vw 2vw; border-radius:1vw;}
    `);

    // 全局变量
    let selectedMatches = new Map();
    let resultContainer = null;
    let isShowing = false;

    // 添加控制按钮
    function addControlButtons() {
        const container = document.createElement('div');
        container.id = 'analysisButtons';
        container.innerHTML = `
            <button id="btnStats">统计</button>
            <button id="btnToggle">显示</button>
        `;

        document.querySelector('div.classTab').appendChild(container);

        // 绑定事件
        document.getElementById('btnStats').addEventListener('click', replaceMintxt);
        document.getElementById('btnToggle').addEventListener('click', toggleAnalysis);
    }

    // 切换显示状态
    async function toggleAnalysis() {
        const btn = document.getElementById('btnToggle');
        if (isShowing) {
            // 隐藏状态
            if (resultContainer) {
                resultContainer.remove();
                resultContainer = null;
            }
            btn.textContent = '显示';
            isShowing = false;
        } else {
            // 显示状态
            btn.textContent = '加载中';
            try {
                await fetchAndRenderData();
                btn.textContent = '隐藏';
                isShowing = true;
            } catch (e) {
                btn.textContent = '显示';
                alert('数据加载失败: ' + e);
            }
        }
    }

    // 替换mintxt为复选框
    function replaceMintxt() {
        document.querySelectorAll('p.mintxt').forEach(p => {
            const parent = p.closest('.leisu-tab-td');
            const link = parent.querySelector('a[href^="/live/detail-"]');
            if (!link) return;
            const matchId = link.href.split('-').pop();
            link.setAttribute('href', link.href.replace('/detail-', '/data-'));
            link.setAttribute('target', '_blank');

            // 获取比分差值
            const scoreText = p.previousElementSibling.textContent;
            const [home, away] = scoreText.split('-').map(Number);
            const diff = home - away;

            // 创建复选框
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.addEventListener('click', function (e) {
                e.stopPropagation();
            });
            checkbox.addEventListener('change', e => {
                if (e.target.checked) {
                    selectedMatches.set(matchId, diff);
                } else {
                    selectedMatches.delete(matchId);
                }
            });

            p.replaceWith(checkbox);
        });
    }

    // 获取并渲染数据
    async function fetchAndRenderData() {
        const timestamp = Date.now();
        const sign = generateRandomString(32);
        const currentMatchId = window.location.pathname.split('-').pop();

        // 获取当前比赛数据
        const currentData = await requestOdds(currentMatchId, sign, timestamp);
        const { initialOdds, liveOdds } = processCurrentData(currentData);

        // 获取选中比赛数据
        const statsRequests = [];
        for (const [matchId, diff] of selectedMatches) {
            statsRequests.push(
                requestOdds(matchId, sign, timestamp)
                    .then(data => ({ matchId, data }))
                    .catch(e => {
                        alert(`比赛 ${matchId} 获取亚盘数据失败: ${e.message}`);
                        return null;
                    })
            );
        }

        const statsResults = await Promise.all(statsRequests);
        const companyStats = processStatsData(statsResults);

        // 渲染结果
        renderAnalysisResult(companyStats, initialOdds, liveOdds);
    }

    // 处理当前比赛数据
    function processCurrentData(data) {
        const initialOdds = new Map();
        const liveOdds = new Map();

        data.data.forEach(companyGroup => {
            const initial = companyGroup.find(e => e.is_begin_odds === 1);
            const live = companyGroup.find(e => e.is_begin_odds === 0);

            if (initial) {
                initialOdds.set(initial.company_name, {
                    home: initial.home_winner,
                    draw: initial.draw,
                    away: initial.away_winner
                });
            }

            if (live) {
                liveOdds.set(live.company_name, {
                    home: live.home_winner,
                    draw: live.draw,
                    away: live.away_winner
                });
            }
        });

        return { initialOdds, liveOdds };
    }

    // 处理统计数据
    function processStatsData(allData) {
        const stats = new Map();

        allData.forEach(({ matchId, data }) => {
            data.data.forEach(companyGroup => {
                companyGroup.forEach(entry => {
                    if (entry.is_begin_odds !== 1) return;

                    const company = entry.company_name;
                    if (!stats.has(company)) {
                        stats.set(company, {
                            count: 0,
                            hit: 0,
                            home: entry.home_winner,
                            draw: entry.draw,
                            away: entry.away_winner
                        });
                    }

                    const record = stats.get(company);
                    record.count++;

                    // 获取当前比赛的差值
                    const diff = selectedMatches.get(matchId);
                    if (typeof diff !== 'number') return;

                    const draw = parseFloat(entry.draw);
                    if (diff === draw) {
                        record.hit++;
                    } else if (diff > draw && entry.home_winner < entry.away_winner) {
                        record.hit++;
                    } else if (diff < draw && entry.away_winner < entry.home_winner) {
                        record.hit++;
                    }
                });
            });
        });

        return stats;
    }

    // 渲染分析结果
    function renderAnalysisResult(stats, initialOdds, liveOdds) {
        // 清理旧容器
        if (resultContainer) {
            resultContainer.remove();
        }

        resultContainer = document.createElement('div');
        resultContainer.className = 'stats-container';

        // 统计表格
        const statsTable = document.createElement('table');
        statsTable.className = 'stats-table';
        statsTable.innerHTML = `
            <thead>
                <tr>
                    <th>公司</th>
                    <th>开盘</th>
                    <th>命中</th>
                    <th>命中率</th>
                    <th>主队</th>
                    <th>让球</th>
                    <th>客队</th>
                </tr>
            </thead>
            <tbody></tbody>
        `;

        // 即时数据表格
        const liveTable = document.createElement('table');
        liveTable.className = 'stats-table';
        liveTable.innerHTML = `
            <thead>
                <tr>
                    <th>公司</th>
                    <th>即时主队</th>
                    <th>即时让球</th>
                    <th>即时客队</th>
                </tr>
            </thead>
            <tbody></tbody>
        `;

        // 填充统计表格
        stats.forEach((data, company) => {
            const row = statsTable.insertRow();
            const initial = initialOdds.get(company) || {};

            row.innerHTML = `
                <td>${company}</td>
                <td>${data.count}</td>
                <td>${data.hit}</td>
                <td>${data.count ? (data.hit / data.count * 100).toFixed(1) + '%' : ''}</td>
                <td>${initial.home || ''}</td>
                <td>${initial.draw || ''}</td>
                <td>${initial.away || ''}</td>
            `;
            highlightCells(row.querySelectorAll('td:nth-child(5), td:nth-child(7)'));
        });

        // 填充即时数据
        liveOdds.forEach((data, company) => {
            const row = liveTable.insertRow();
            row.innerHTML = `
                <td>${company}</td>
                <td>${data.home}</td>
                <td>${data.draw}</td>
                <td>${data.away}</td>
            `;
            highlightCells(row.querySelectorAll('td:nth-child(2), td:nth-child(4)'));
        });

        // 组装容器
        resultContainer.append(statsTable, liveTable);
        document.querySelector('div.classTab').after(resultContainer);
    }

    // 辅助函数
    function highlightCells(cells) {
        if (cells.length !== 2) return;

        const [cellA, cellB] = cells;
        const valueA = parseFloat(cellA.textContent);
        const valueB = parseFloat(cellB.textContent);

        cellA.classList.remove('highlight-green', 'highlight-red');
        cellB.classList.remove('highlight-green', 'highlight-red');

        if (isNaN(valueA) || isNaN(valueB)) return;

        if (valueA < valueB) {
            cellA.classList.add('highlight-green');
            cellB.classList.add('highlight-red');
        } else if (valueA > valueB) {
            cellA.classList.add('highlight-red');
            cellB.classList.add('highlight-green');
        }
    }

    function generateRandomString(length) {
        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
    }

    function requestOdds(matchId, sign, timestamp) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://pay.jcyqr.com/odds?sign=${sign}&timestamp=${timestamp}&match_id=${matchId}&type=4`,
                onload: (res) => {
                    if (res.status === 200) {
                        try {
                            resolve(JSON.parse(res.responseText));
                        } catch (e) {
                            reject(new Error('数据解析失败'));
                        }
                    } else {
                        reject(new Error(`HTTP ${res.status}`));
                    }
                },
                onerror: (err) => reject(err)
            });
        });
    }

    //链接替换成数据页面并在新窗口打开
    function modifyLink(a) {
        const originalHref = a.getAttribute('href');
        if (originalHref && originalHref.includes('/live/detail-')) {
            const newHref = originalHref.replace('/live/detail-', '/live/data-');
            a.setAttribute('href', newHref);
        }
        a.setAttribute('target', '_blank');
    }

    function livePageMod() {
        document.getElementById('MatchTopBanner').style.display = "none";
        // 初始处理已有链接
        document.querySelectorAll('#ftb_live li a.linkk').forEach(a => {
            modifyLink(a);
        });

        // 使用MutationObserver监控动态加载的内容
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE && node.matches('li.ftb-lier-base')) {
                        const link = node.querySelector('a.linkk');
                        if (link) modifyLink(link);
                    }
                }
            }
        });

        // 开始观察列表容器
        const listContainer = document.getElementById('ftb_live');
        if (listContainer) {
            observer.observe(listContainer, {
                childList: true,
                subtree: false
            });
        }
    }

    //根据网址初始化
    if (/^https:\/\/m\.leisu\.com\/live\/?$/.test(window.location.href)) {
        setTimeout(livePageMod, 4000);
    } else if (window.location.pathname.startsWith('/live/data-')) {
        setTimeout(addControlButtons, 4000);
    }

})();