// ==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>
`;
}
})();