您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
打开豆瓣影视的具体页面,将展示三位小数的评分、历史折线图。
// ==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">×</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">×</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">×</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">×</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> `; } })();