追剧查评分-豆瓣

打开豆瓣影视的具体页面,将展示三位小数的评分、历史折线图。

// ==UserScript==
// @name         追剧查评分-豆瓣
// @namespace    http://tampermonkey.net/
// @version      1.4.8
// @description  打开豆瓣影视的具体页面,将展示三位小数的评分、历史折线图。
// @author       interest2
// @match        https://movie.douban.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      www.ratetend.com
// @license      GPL-3.0-only
// ==/UserScript==

(function () {
    'use strict';
    console.log("ai script, start");

    const version = "1.4.8";
    const API_HOST = "https://www.ratetend.com";
    // const API_HOST = "http://localhost:2001";

    const TOOL_PREFIX = "ratetend_";
    const rateCss = '.rating_self strong';
    let latestDate = "";
    const UUID_KEY = TOOL_PREFIX + 'uuid';
    const YYYY_TO_MINUTE = "yyyy-MM-dd hh:mm";
    checkFirstRun();

    // 立即尝试检测(脚本开始执行时)
    if (document.readyState === 'loading') {
        // 如果DOM还在加载中,等待DOMContentLoaded
        console.log('DOM还在加载中,等待DOMContentLoaded事件');
    } else if (document.readyState === 'interactive') {
        // 如果DOM已加载完成但资源还在加载
        console.log('DOM已加载完成,立即检测');
        if (document.querySelector(rateCss)) {
            console.log('立即检测到评分元素');
            setTimeout(() => handleMovieData(), 0); // 异步执行,确保其他初始化完成
        }
    } else if (document.readyState === 'complete') {
        // 如果页面完全加载完成
        console.log('页面完全加载完成,立即检测');
        if (document.querySelector(rateCss)) {
            console.log('立即检测到评分元素');
            setTimeout(() => handleMovieData(), 0);
        }
    }
    
    loadECharts(() => {});

    // 使用MutationObserver监听DOM变化,尽早检测 rateEl 元素
    let isObserving = false; // 防止重复启动监听器
    let hasExecuted = false; // 防止重复执行handleMovieData
    
    function startObserving() {
        if (isObserving) {
            console.log('监听器已在运行中');
            return;
        }
        
        isObserving = true;
        console.log('启动DOM监听器');
        
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.type === 'childList') {
                    // 检查新添加的节点中是否包含目标元素
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // 检查新节点本身
                            if (node.matches && node.matches(rateCss)) {
                                console.log('MutationObserver检测到评分元素');
                                observer.disconnect(); // 找到后立即停止监听
                                isObserving = false;
                                if (!hasExecuted) {
                                    handleMovieData();
                                }
                                return;
                            }
                            // 检查新节点的子元素
                            if (node.querySelector && node.querySelector(rateCss)) {
                                console.log('MutationObserver检测到评分元素');
                                observer.disconnect(); // 找到后立即停止监听
                                isObserving = false;
                                if (!hasExecuted) {
                                    handleMovieData();
                                }
                                return;
                            }
                        }
                    }
                }
            }
        });

        // 开始监听
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 设置超时,避免无限等待
        setTimeout(() => {
            if (isObserving) {
                observer.disconnect();
                isObserving = false;
                console.log('DOM监听超时,尝试直接检测');
                // 超时后尝试直接检测
                if (document.querySelector(rateCss)) {
                    handleMovieData();
                }
            }
        }, 10000); // 10秒超时

        return observer;
    }

    // 尽早检测评分元素
    function tryHandleMovieData() {
        if (hasExecuted) {
            console.log('handleMovieData已经执行过,跳过');
            return true;
        }
        
        if (document.querySelector(rateCss)) {
            console.log('检测到评分元素,触发handleMovieData');
            // 如果正在监听,立即停止
            if (isObserving) {
                console.log('停止DOM监听器');
                isObserving = false;
            }
            handleMovieData();
            return true;
        }
        return false;
    }

    // DOM内容加载完成时就开始检测
    document.addEventListener('DOMContentLoaded', function() {
        console.log('DOMContentLoaded');
        
        if (!tryHandleMovieData()) {
            console.log('DOMContentLoaded时未找到评分元素,开始监听');
            startObserving();
        }
    });

    window.onload = function() {
        console.log("onload");

        // 再次尝试检测(以防DOMContentLoaded时还没加载完成)
        if (!tryHandleMovieData()) {
            console.log('onload时仍未找到评分元素,继续监听');
            // 如果DOMContentLoaded时已经开始监听,这里就不需要重复启动
        }
    };

    // 动态加载 ECharts 库
    function loadECharts(callback) {
        if (typeof echarts !== 'undefined') {
            echartsLoaded = true;
            callback();
            return;
        }

        const script = document.createElement('script');
        script.src = 'https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js';
        script.onload = () => {
            console.log('ECharts 加载完成');
            echartsLoaded = true;
            callback();
        };
        document.head.appendChild(script);
    }

    // 星级 → 分值
    var SCORE = [10, 8, 6, 4, 2];
    let history;
    let echartsLoaded = false; // 标记ECharts是否已加载完成
    let dataReady = false; // 标记数据是否已准备完成

    // 解析、提交、获取
    async function handleMovieData(){
        // 防重复执行
        if (hasExecuted) {
            console.log('handleMovieData 已经执行过,跳过');
            return;
        }

        hasExecuted = true;
        console.log('开始执行 handleMovieData');
        
        // 确保停止DOM监听器,避免重复触发
        if (isObserving) {
            console.log('handleMovieData执行,停止DOM监听器');
            isObserving = false;
        }

        let data = parsePageRate();
        addRateButtons(data.tag, data.year);

        try {
            // 等待提交数据完成
            await commitRateData(data);
            console.log('数据提交完成');
            
            // 等待获取分享数据完成
            let fetchRet = await fetchShareData({"movieid": extractMovieid()});
            history = fetchRet.saves;
            latestDate = commonFormatDate(fetchRet.latestDate, "MM-dd hh:mm");
            console.log('数据获取完成,历史数据条数:', history ? history.length : 0);
            
            // 等待ECharts加载完成
            if (!echartsLoaded) {
                console.log('等待ECharts加载完成...');
                await new Promise((resolve) => {
                    const checkEcharts = () => {
                        if (echartsLoaded) {
                            resolve();
                        } else {
                            setTimeout(checkEcharts, 100);
                        }
                    };
                    checkEcharts();
                });
                console.log('ECharts加载完成');
            }
            
            // 所有数据准备完成,设置标记
            dataReady = true;
            console.log('所有数据准备完成,可以显示图表');
        } catch (error) {
            console.error('handleMovieData执行失败:', error);
            hasExecuted = false; // 重置执行状态,允许重试
            dataReady = false; // 重置数据准备状态
        }
    }

    function parsePageRate() {
        var rateEl = document.querySelector(rateCss);
        var people = document.querySelector('.rating_people span');
        var stars = document.querySelectorAll('.ratings-on-weight .rating_per');

        let titleEl = document.querySelector('h1 [property="v:itemreviewed"]');
        if (!titleEl) return; // Not a movie page
        let title = titleEl.textContent;
        let yearEl = document.querySelector('h1 .year');
        let yearMatch = yearEl.textContent.match(/\d+/);
        let year = yearMatch[0];

        let actors = [...document.querySelectorAll('a[rel="v:starring"]')]
            .map(a => a.textContent.trim()).slice(0, 4).join(' / ');

        let hasValidData = people && stars.length === 5 && rateEl;
        if(!hasValidData) return;

        var rateSum = 0;
        var starSum = 0;
        let starArray = [];

        for (var i = 0; i < stars.length; i++) {
            var star = parseFloat(stars[i].innerText);
            rateSum += SCORE[i] * star;
            starSum += star;
            starArray.push(star);
        }

        let rate = rateEl.textContent;
        let rateAvg = rateSum / 100.0;
        let rateDetail = correctRealRate(starSum, rateAvg);
        rateEl.appendChild(document.createTextNode(' (' + rateDetail + ')'));

        let apiKey = localStorage.getItem(TOOL_PREFIX + "api_key");
        if(isEmpty(apiKey)) apiKey = "";

        let recordDate = (new Date()).format(YYYY_TO_MINUTE);
        const uuid = localStorage.getItem(UUID_KEY);

        return {
            "type": 4,
            "tag": title,
            "movieid": extractMovieid(),
            "year": year,
            "actors": actors,
            "star": starArray.toString(),
            "rate": rate,
            "rateDetail": rateDetail,
            "people": people.textContent,
            "date": recordDate,
            "apiKey": apiKey,
            "uuid": uuid,
            "version": version
        };
    }

    function addRateButtons(title, year) {
        // 检查是否已经添加过按钮
        if (document.querySelector('.chart-buttons')) return;

        var rate = document.querySelector('.rating_self strong');

        const buttonsContainer = document.createElement('div');
        buttonsContainer.className = 'chart-buttons';

        const chartBtn = document.createElement('button');
        chartBtn.className = 'rate-btn';
        chartBtn.textContent = '折线图';
        chartBtn.onclick = async () => await showChartsPopup(title, year);

        const tableBtn = document.createElement('button');
        tableBtn.className = 'rate-btn';
        tableBtn.textContent = '表格';
        tableBtn.onclick = () => showTablePopup(title, year);

        // 新增API Key设置按钮
        const apiKeyBtn = document.createElement('button');
        apiKeyBtn.className = 'rate-btn';
        apiKeyBtn.textContent = '设置';
        apiKeyBtn.onclick = () => showApiKeyPopup();

        buttonsContainer.appendChild(chartBtn);
        buttonsContainer.appendChild(tableBtn);
        buttonsContainer.appendChild(apiKeyBtn);

        rate.insertAdjacentElement('afterend', buttonsContainer);
    }

    // 缓存图表实例,切换时触发 resize
    let chartInstances = { rating: null, star: null, people: null };

    async function showChartsPopup(title, year) {
        console.log("展示折线图");

        // 检查ECharts是否已加载,如果未加载则等待加载完成
        if (!echartsLoaded || typeof echarts === 'undefined') {
            console.log('ECharts未加载完成,等待加载...');
            await new Promise((resolve) => {
                const checkEcharts = () => {
                    if (echartsLoaded && typeof echarts !== 'undefined') {
                        console.log('ECharts加载完成,继续执行');
                        resolve();
                    } else {
                        setTimeout(checkEcharts, 100);
                    }
                };
                checkEcharts();
            });
        }

        // 检查数据是否已准备完成
        if (!dataReady) {
            console.log('数据尚未准备完成,等待...');
            await new Promise((resolve) => {
                const checkDataReady = () => {
                    if (dataReady) {
                        console.log('数据准备完成,继续执行');
                        resolve();
                    } else {
                        setTimeout(checkDataReady, 100);
                    }
                };
                checkDataReady();
            });
        }

        if (!history || history.length === 0) {
            alert('暂无历史数据');
            return;
        }

        const popup = createPopup();
        const content = popup.querySelector('.popup-content');

        content.innerHTML = `
            <button class="popup-close">&times;</button>
            <h2>${title} (${year}) 折线图</h2>
            <span style="font-size: 16px">更新于 ${latestDate}</span>
            <div class="chart-tabs">
                <button class="chart-tab active" data-chart="rating">评分</button>
                <button class="chart-tab" data-chart="star">星级</button>
                <button class="chart-tab" data-chart="people">人数</button>
            </div>
            <div class="chart-stack">
                <div id="ratingChart" class="chart-container"></div>
                <div id="starChart" class="chart-container hidden-chart"></div>
                <div id="peopleChart" class="chart-container hidden-chart"></div>
            </div>
        `;

        document.body.appendChild(popup);

        // 一次性渲染所有图表
        setTimeout(() => {
            ratingChart(history);
            starChart(history);
            peopleChart(history);
        }, 10);

        // 添加选项卡切换事件
        const tabs = content.querySelectorAll('.chart-tab');
        tabs.forEach(tab => {
            tab.onclick = () => switchChartTab(tab.dataset.chart, tabs);
        });

        // 关闭按钮事件
        content.querySelector('.popup-close').onclick = () => {
            try {
                chartInstances.rating && chartInstances.rating.dispose();
                chartInstances.star && chartInstances.star.dispose();
                chartInstances.people && chartInstances.people.dispose();
            } catch (e) {}
            chartInstances = { rating: null, star: null, people: null };
            document.body.removeChild(popup);
        };

        // 点击遮罩关闭
        popup.onclick = (e) => {
            if (e.target === popup) {
                try {
                    chartInstances.rating && chartInstances.rating.dispose();
                    chartInstances.star && chartInstances.star.dispose();
                    chartInstances.people && chartInstances.people.dispose();
                } catch (e) {}
                chartInstances = { rating: null, star: null, people: null };
                document.body.removeChild(popup);
            }
        };
    }

    // 选项卡切换函数
    function switchChartTab(chartType, tabs) {
        // 移除所有选项卡的active类
        tabs.forEach(tab => tab.classList.remove('active'));

        // 隐藏所有图表(不使用display: none,避免宽度为0)
        document.getElementById('ratingChart').classList.add('hidden-chart');
        document.getElementById('starChart').classList.add('hidden-chart');
        document.getElementById('peopleChart').classList.add('hidden-chart');

        // 激活当前选项卡
        const activeTab = document.querySelector(`[data-chart="${chartType}"]`);
        activeTab.classList.add('active');

        // 显示对应图表并触发resize
        const targetId = chartType + 'Chart';
        const el = document.getElementById(targetId);
        el.classList.remove('hidden-chart');
        const inst = chartInstances[chartType];
        if (inst && typeof inst.resize === 'function') {
            inst.resize();
        }
    }

    function showTablePopup(title, year) {
        if (history.length === 0) {
            alert('暂无历史数据');
            return;
        }
        const popup = createPopup();
        const content = popup.querySelector('.popup-content');

        // 按时间倒序排列,最新的记录在前面
        const sortedHistory = [...history].sort((a, b) => new Date(b.recordDate) - new Date(a.recordDate));
        content.innerHTML = TABLE_TEMPLATE(sortedHistory, title, year);

        document.body.appendChild(popup);

        // 关闭按钮事件
        content.querySelector('.popup-close').onclick = () => {
            document.body.removeChild(popup);
        };

        // 点击遮罩关闭
        popup.onclick = (e) => {
            if (e.target === popup) {
                document.body.removeChild(popup);
            }
        };
    }

    function createPopup() {
        const popup = document.createElement('div');
        popup.className = 'popup-overlay';
        popup.innerHTML = '<div class="popup-content"></div>';
        return popup;
    }

    // 公共工具函数
    function formatDates(history) {
        return history.map(item => {
            return commonFormatDate(item.recordDate, "MM-dd");
        });
    }

    // 格式化单个日期
    function commonFormatDate(dateStr, template) {
        return yearPrefix(dateStr) + new Date(dateStr).format(template);
    }

    function yearPrefix(dateStr) {
        const date = new Date(dateStr);
        const currentYear = new Date().getFullYear();
        const dateYear = date.getFullYear();

        return dateYear === currentYear ? "" : dateYear + "-";
    }

    // 判断是否为当天数据
    function isToday(dateStr) {
        const date = new Date(dateStr);
        const today = new Date();
        return date.toDateString() === today.toDateString();
    }

    // 按天聚合数据,每天只保留最晚的记录
    function aggregateByDay(history) {
        const dailyMap = new Map();
        
        history.forEach(item => {
            const date = new Date(item.recordDate);
            const dayKey = date.toISOString().split('T')[0]; // 获取 YYYY-MM-DD 格式的日期
            
            if (!dailyMap.has(dayKey) || new Date(item.recordDate) > new Date(dailyMap.get(dayKey).recordDate)) {
                dailyMap.set(dayKey, item);
            }
        });
        
        // 转换为数组并按日期排序
        return Array.from(dailyMap.values()).sort((a, b) => new Date(a.recordDate) - new Date(b.recordDate));
    }

    // 创建series的工具函数,自动应用公共配置
    function createSeries(oneFlag, seriesConfig, commonConfig = {}) {
        // 当history只有1条记录时,使用bar类型;否则使用line类型
        const chartType = oneFlag ? 'bar' : 'line';
        
        const defaultConfig = {
            symbol: 'none',  // 默认隐藏小圆点
            type: chartType,
            ...commonConfig
        };

        if (Array.isArray(seriesConfig)) {
            return seriesConfig.map(config => ({ ...defaultConfig, ...config }));
        }
        return { ...defaultConfig, ...seriesConfig };
    }

    // 基础option配置生成器
    function createBaseOption(title, legendData, dates, customTooltip = null) {
        // 计算interval值:当数据点超过10个时,动态调整显示密度
        const dataLength = dates.length;
        let interval = 'auto';
        if (dataLength > 10) {
            // 计算interval,使显示的标签数量控制在10个左右
            interval = Math.floor(dataLength / 10);
            // 确保interval至少为1
            if (interval < 1) interval = 1;
        }

        const option = {
            title: {
                text: title,
                left: 'center',
                textStyle: { fontSize: 20, color: '#333' }
            },
            tooltip: {
                trigger: 'axis',
                axisPointer: { type: 'cross' }
            },
            legend: { data: legendData, top: 30 },
            grid: {
                left: '3%', right: '4%', bottom: '3%', top: '20%',
                containLabel: true
            },
            xAxis: {
                type: 'category',
                boundaryGap: false,
                data: dates,
                axisLabel: {
                    rotate: 45,
                    fontSize: 14,
                    interval: interval
                }
            }
        };

        if (customTooltip) {
            option.tooltip = { ...option.tooltip, ...customTooltip };
        }

        return option;
    }

    // 评分图表
    function ratingChart(history) {
        const chartContainer = document.getElementById('ratingChart');
        if (!chartContainer || typeof echarts === 'undefined') return;

        try { chartInstances.rating && chartInstances.rating.dispose(); } catch (e) {}
        const myChart = echarts.init(chartContainer);
        chartInstances.rating = myChart;
        
        // 按天聚合数据
        const aggregatedHistory = aggregateByDay(history);
        const dates = formatDates(aggregatedHistory);
        const rates = aggregatedHistory.map(item => parseFloat(item.rate));
        const rateDetails = aggregatedHistory.map(item => parseFloat(item.rateDetail));

        const minRate = Math.min(...rates, ...rateDetails);
        const maxRate = Math.max(...rates, ...rateDetails);
        const delta = 0.05;
        const dynamicMin = Math.max(0, minRate - delta);
        const dynamicMax = maxRate + delta;

        const option = createBaseOption('评分趋势', ['评分', '小分'], dates);

        option.yAxis = {
            type: 'value', name: '评分',
            min: dynamicMin.toFixed(2), max: dynamicMax.toFixed(2),
            minInterval: 0.05,
            axisLabel: { textStyle: { fontSize: 14 } },
            splitLine: { show: false }
        };

        option.series = createSeries(dates.length === 1, [
            { name: '评分', data: rates, lineStyle: { color: "#275fe6", width: 3 }, itemStyle: { color: "#275fe6" } },
            { name: '小分', data: rateDetails, lineStyle: { color: "#eb3c10", width: 2 }, itemStyle: { color: "#eb3c10" } }
        ]);

        myChart.setOption(option);
        myChart.resize();
    }

    // 星级图表
    function starChart(history) {
        const chartContainer = document.getElementById('starChart');
        if (!chartContainer || typeof echarts === 'undefined') return;

        try { chartInstances.star && chartInstances.star.dispose(); } catch (e) {}
        const myChart = echarts.init(chartContainer);
        chartInstances.star = myChart;
        
        // 按天聚合数据
        const aggregatedHistory = aggregateByDay(history);
        const dates = formatDates(aggregatedHistory);

        const starData = [[], [], [], [], []];
        aggregatedHistory.forEach(item => {
            const starsArray = item.star ? item.star.split(',').map(s => parseFloat(s.trim())) : [0, 0, 0, 0, 0];
            starData.forEach((arr, index) => arr.push(starsArray[index] || 0));
        });

        const starColors = ["#fc4646", "#fc8132", "#c0c0c0", "#60aaf9", "#0bb73b"];
        const starNames = ['五星', '四星', '三星', '二星', '一星'];

        const option = createBaseOption('星级分布趋势', starNames, dates);

        option.yAxis = {
            type: 'value', name: '单位(%)', min: 0, minInterval: 0.05,
            axisLabel: { formatter: '{value}', textStyle: { fontSize: 14 } },
            splitLine: { show: false }
        };

        option.series = createSeries(dates.length === 1,
            starData.map((data, index) => ({
                name: starNames[index], data: data,
                lineStyle: { color: starColors[index], width: 2 },
                itemStyle: { color: starColors[index] }
            }))
        );

        myChart.setOption(option);
        myChart.resize();
    }

    // 人数图表
    function peopleChart(history) {
        const chartContainer = document.getElementById('peopleChart');
        if (!chartContainer || typeof echarts === 'undefined') return;

        try { chartInstances.people && chartInstances.people.dispose(); } catch (e) {}
        const myChart = echarts.init(chartContainer);
        chartInstances.people = myChart;
        
        // 按天聚合数据
        const aggregatedHistory = aggregateByDay(history);
        const dates = formatDates(aggregatedHistory);

        const originalPeople = aggregatedHistory.map(item => {
            const peopleStr = item.people.toString();
            const match = peopleStr.match(/\d+/);
            return match ? parseInt(match[0]) : 0;
        });

        const people = originalPeople.map(num => num / 1000);
        const minPeople = Math.min(...people);
        const maxPeople = Math.max(...people);
        let delta = maxPeople - minPeople;
        delta = delta < 1 ? 1 : delta;

        let dynamicMin, dynamicMax;
        if (delta < maxPeople / 10) {
            dynamicMin = Math.round(maxPeople - delta * 10);
            dynamicMax = Math.round(maxPeople + delta);
        } else {
            dynamicMin = 0;
            dynamicMax = undefined;
        }

        const customTooltip = {
            formatter: function(params) {
                const dataIndex = params[0].dataIndex;
                const date = params[0].axisValue;
                const originalValue = originalPeople[dataIndex];
                return `${date}<br/>评分人数: ${originalValue.toLocaleString()}人`;
            }
        };

        const option = createBaseOption('评分人数趋势', ['评分人数'], dates, customTooltip);

        option.yAxis = {
            type: 'value', name: '单位:千人',
            min: dynamicMin, max: dynamicMax, minInterval: 1,
            axisLabel: { textStyle: { fontSize: 14 } },
            splitLine: { show: false }
        };

        option.series = createSeries(dates.length === 1, {
            name: '评分人数', data: people,
            lineStyle: { color: "#275fe6", width: 3 },
            itemStyle: { color: "#275fe6" },
            areaStyle: {
                color: {
                    type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
                    colorStops: [
                        { offset: 0, color: 'rgba(39, 95, 230, 0.3)' },
                        { offset: 1, color: 'rgba(39, 95, 230, 0.1)' }
                    ]
                }
            }
        });

        myChart.setOption(option);
        myChart.resize();
    }

    function fetchShareData(data){
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: API_HOST + "/web/browserSaves",
                data: JSON.stringify(data),
                headers: {
                    "Content-Type": "application/json"
                },
                onload: function(response) {
                    try {
                        let ret = JSON.parse(response.responseText);
                        resolve(ret.data || []);
                    } catch (e) {
                        console.error('解析响应失败:', e);
                        resolve([]);
                    }
                },
                onerror: function(error) {
                    console.error('请求失败:', error);
                    resolve([]);
                }
            });
        });
    }

    function commitRateData(data){
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: API_HOST + "/web/commitRateData",
                data: JSON.stringify(data),
                headers: {
                    "Content-Type": "application/json"
                },
                onload: function(response) {
                    console.log(response.responseText);
                    resolve(response);
                },
                onerror: function(error) {
                    console.error('请求失败:', error);
                    reject(error);
                }
            });
        });
    }

    function correctRealRate(starSum, rate) {
        starSum = starSum.toFixed(1) * 10
        var rM;
        switch (starSum){
            case 999:
                rM = rate + 0.006;
                break;
            case 998:
                rM = rate + 0.012;
                break;
            case 1001:
                rM = rate - 0.006;
                break;
            case 1002:
                rM = rate - 0.012;
                break;
            default:
                rM = rate;
        }
        return rM.toFixed(3);
    }

    function extractMovieid(){
        let url = window.location.href;
        const parts = url.split('/');
        const index = parts.indexOf('subject');
        return index !== -1 ? parts[index + 1] : '';
    }


    // 首次运行检测
    function checkFirstRun() {
        const uuid = localStorage.getItem(UUID_KEY);

        if (!uuid) {
            // 生成UUID并存储
            const uuid = generateUUID();
            localStorage.setItem(UUID_KEY, uuid);

            // 显示首次运行提示弹窗
            showFirstRunPopup();
        }
    }

    // 首次运行提示弹窗
    function showFirstRunPopup() {
        const popup = createPopup();
        const content = popup.querySelector('.popup-content');

        content.innerHTML = FIRST_RUN_TEMPLATE();

        document.body.appendChild(popup);

        // 关闭按钮事件
        content.querySelector('.popup-close').onclick = () => {
            document.body.removeChild(popup);
        };

        // 点击遮罩关闭
        popup.onclick = (e) => {
            if (e.target === popup) {
                document.body.removeChild(popup);
            }
        };
    }

    // API Key设置弹窗
    function showApiKeyPopup() {
        const popup = createPopup();
        const content = popup.querySelector('.popup-content');

        const currentApiKey = localStorage.getItem(TOOL_PREFIX + "api_key") || '';

        content.innerHTML = API_KEY_TEMPLATE(currentApiKey);

        document.body.appendChild(popup);

        // 保存API Key函数
        function saveApiKey() {
            const apiKey = document.getElementById('apiKeyInput').value.trim();
            if (apiKey) {
                localStorage.setItem(TOOL_PREFIX + "api_key", apiKey);
                alert('API Key 保存成功!');
                document.body.removeChild(popup);
            } else {
                alert('请输入有效的API Key!');
            }
        }

        // 绑定事件监听器
        content.querySelector('#saveBtn').onclick = saveApiKey;
        content.querySelector('#cancelBtn').onclick = () => {
            document.body.removeChild(popup);
        };

        // 关闭按钮事件
        content.querySelector('.popup-close').onclick = () => {
            document.body.removeChild(popup);
        };

        // 点击遮罩关闭
        popup.onclick = (e) => {
            if (e.target === popup) {
                document.body.removeChild(popup);
            }
        };

        // 聚焦输入框
        setTimeout(() => {
            document.getElementById('apiKeyInput').focus();
        }, 100);

        // 支持回车键保存
        document.getElementById('apiKeyInput').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                saveApiKey();
            }
        });
    }


    /**
     * 通用工具
     * */
    Date.prototype.format = function(format) {
        var o = {
            "M+" : this.getMonth() + 1, // month
            "d+" : this.getDate(), // day
            "h+" : this.getHours(), // hour
            "m+" : this.getMinutes(), // minute
            "s+" : this.getSeconds(), // second
            "q+" : Math.floor((this.getMonth() + 3) / 3), // quarter
            "S" : this.getMilliseconds()
        };
        if (/(y+)/.test(format)) {
            format = format.replace(RegExp.$1, (this.getFullYear() + "")
                .substr(4 - RegExp.$1.length));
        }
        for ( var k in o) {
            if (new RegExp("(" + k + ")").test(format)) {
                format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k]
                    : ("00" + o[k]).substr(("" + o[k]).length));
            }
        }
        return format;
    };

    function isEmpty(item) {
        if (item === null || item === undefined || item.length === 0 || item === "null") {
            return true;
        } else {
            return false;
        }
    }

    // 生成简单的UUID
    function generateUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxxx'.replace(/[xy]/g, function (c) {
            var r = Math.random() * 16 | 0,
                v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    // 添加弹窗样式
    GM_addStyle(`
        .popup-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
        }
        .popup-content {
            background: white;
            padding: 20px;
            border-radius: 8px;
            max-width: 1500px;
            min-width: 600px;
            max-height: 90vh;
            overflow-y: auto;
            position: relative;
        }
        .popup-close {
            position: absolute;
            top: 10px;
            right: 15px;
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            color: #999;
        }
        .popup-close:hover {
            color: #333;
        }
        .chart-buttons {
            margin-left: 10px;
            display: inline-block;
        }
        .rate-btn {
            background: #ec7258;
            color: white;
            border: none;
            padding: 3px;
            margin: 3px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
        }
        .rate-btn:hover {
            background: #369647;
        }
        .first-run-content {
            line-height: 1.6;
        }
        .first-run-content ul {
            margin: 10px 0;
            padding-left: 20px;
        }
        .first-run-content li {
            margin: 5px 0;
        }
        .api-key-input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
            margin: 10px 0;
            box-sizing: border-box;
        }
        .popup-buttons {
            text-align: right;
            margin-top: 20px;
        }
        .popup-btn {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            margin-left: 10px;
        }
        .popup-btn-primary {
            background: #ec7258;
            color: white;
        }
        .popup-btn-primary:hover {
            background: #369647;
        }
        .popup-btn-secondary {
            background: #f5f5f5;
            color: #333;
        }
        .popup-btn-secondary:hover {
            background: #e0e0e0;
        }
        .chart-container {
            width: 100%;
            height: 400px;
            margin-top: 2px;
            border: 1px solid #ddd;
        }
        .chart-stack {
            position: relative;
            width: 100%;
            height: 400px;
            margin-top: 20px;
        }
        .chart-stack .chart-container {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            margin-top: 0;
        }
        /* 使用visibility和position隐藏,保留尺寸参与布局,避免ECharts宽度为0 */
        .hidden-chart {
            visibility: hidden;
            position: absolute;
            left: -99999px;
        }
        .chart-tabs {
            display: flex;
            margin-top: 2px;
            border-bottom: 1px solid #ddd;
        }
        .chart-tab {
            background: none;
            border: none;
            padding: 12px 24px;
            cursor: pointer;
            font-size: 14px;
            color: #666;
            border-bottom: 2px solid transparent;
            transition: all 0.3s ease;
        }
        .chart-tab:hover {
            color: #333;
            background: #f5f5f5;
        }
        .chart-tab.active {
            color: #ec7258;
            border-bottom-color: #ec7258;
            font-weight: bold;
        }
        .popup-table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 10px;
        }
        .popup-table th, .popup-table td {
            border: 1px solid #ddd;
            padding: 8px 12px;
            text-align: left;
            font-size: 13px;
        }
        .popup-table th {
            background-color: #f5f5f5;
            font-weight: bold;
            color: #666;
        }
        .popup-table tr:nth-child(even) {
            background-color: #fafafa;
        }
        .popup-table .star-col {
            text-align: center;
            min-width: 50px;
        }

        /* 响应式设计 - 在小屏幕上调整 */
        @media (max-width: 900px) {
            .popup-content {
                width: 95%;
                min-width: 300px;
                max-width: none;
            }
        }
    `);

// HTML模板常量函数
    function FIRST_RUN_TEMPLATE() {
        return `
    <button class="popup-close">&times;</button>
    <h2>欢迎使用追剧查评分脚本!</h2>
    <div class="first-run-content">
        <p>🎬 功能:</p>
        <ul>
            <li>每次打开或刷新页面,展示三位小数的评分,并帮您记录到云端;</li>
            <li>在云端与其他用户的数据汇聚,从而“众筹”得到评分折线图,展示在本页面</li>
        </ul>
        <p>💡 其他说明:</p>
        <ul>
            <li>
                本工具的<a href="https://www.ratetend.com" target="_blank"> 网站 </a>还可支持截图算分等多种算分方式
            </li>
            <li>
               即折线图的数据,均来自各用户手动上传,包括:<br>
               ① 您打开本网页时利用插件脚本进行的上传,② 在上述工具网站通过截图OCR识别、手动算分等方式进行的记录。
            </li>
        </ul>
        <div class="popup-buttons">
            <button class="popup-btn popup-btn-primary" onclick="this.closest('.popup-overlay').remove()">开始使用</button>
        </div>
    </div>
`;
    }

    function API_KEY_TEMPLATE(currentApiKey) {
        return `
    <button class="popup-close">&times;</button>
    <h2>API Key 设置</h2>
    <div class="api-key-content">
        <p>API Key 是指您在“追剧查评分”网站的身份凭证,<br>
        填写它后,后续打开或刷新本页面,评分数据就能关联到您账号下创建的对应影视。<br>
        获取方式:在 <a href="https://www.ratetend.com/web/mySettings" target="_blank">该网站</a> 登录后,在“账号设置”菜单界面获取。</p>
        <input type="text" id="apiKeyInput" class="api-key-input" placeholder="请填写api key" value="${currentApiKey}">
        <div class="popup-buttons">
            <button class="popup-btn popup-btn-secondary" id="cancelBtn">取消</button>
            <button class="popup-btn popup-btn-primary" id="saveBtn">保存</button>
        </div>
    </div>
`;
    }

    function TABLE_TEMPLATE(history, title, year) {
        return `
    <button class="popup-close">&times;</button>
    <h2>${title} (${year}) 评分历史记录</h2>
    <table class="popup-table">
        <thead>
            <tr>
                <th>时间</th>
                <th>评分</th>
                <th>小分</th>
                <th>人数</th>
                <th class="star-col">五星</th>
                <th class="star-col">四星</th>
                <th class="star-col">三星</th>
                <th class="star-col">二星</th>
                <th class="star-col">一星</th>
                <th class="star-col">合计</th>
            </tr>
        </thead>
        <tbody>
            ${history.map(item => {
        const starsArray = item.star ? item.star.split(',').map(s => parseFloat(s.trim())) : [0, 0, 0, 0, 0];
        // 计算五星到一星的总和,保留一位小数
        const total = starsArray.reduce((sum, star) => sum + star, 0).toFixed(1);
        // 处理日期显示逻辑
        const displayDate = commonFormatDate(item.recordDate, "MM-dd hh点");
        // 判断是否为当天数据,如果是则添加红色样式
        const timeStyle = isToday(item.recordDate) ? 'style="color: #eb3c10;"' : '';
        return `
                <tr>
                    <td ${timeStyle}>${displayDate}</td>
                    <td>${item.rate}</td>
                    <td>${item.rateDetail}</td>
                    <td>${item.people}</td>
                    <td class="star-col">${starsArray[0] || 0}</td>
                    <td class="star-col">${starsArray[1] || 0}</td>
                    <td class="star-col">${starsArray[2] || 0}</td>
                    <td class="star-col">${starsArray[3] || 0}</td>
                    <td class="star-col">${starsArray[4] || 0}</td>
                    <td class="star-col">${total}</td>
                </tr>
                `;
    }).join('')}
        </tbody>
    </table>
`;
    }

})();