您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动统计YouTube评论区的捐款信息,支持多种货币
当前为
// ==UserScript== // @name YouTube捐款统计器 // @namespace https://github.com/yourusername/yt-donation-tracker // @version 1.0.0 // @description 自动统计YouTube评论区的捐款信息,支持多种货币 // @author Jorkey Liu // @match https://www.youtube.com/watch* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @license MIT // ==/UserScript== (function() { 'use strict'; // ========================= // 常量定义 // ========================= const CONFIG = { debug: true, // 调试模式 scanInterval: 1000, // 扫描间隔(毫秒) }; const SELECTORS = { commentsSection: 'ytd-comments#comments', commentThreads: 'ytd-comment-thread-renderer', commentContent: '#content-text', authorText: '#author-text', loadingIndicator: 'yt-next-continuation', // 捐款相关选择器 donationChipPrice: 'span#comment-chip-price, .yt-spec-button-shape-next__button-text-content', // 捐款价格元素,包括一般超级留言和其他按钮文本 priceElements: '[id*="price"], [class*="price"], span.style-scope.yt-formatted-string' // 通用价格元素选择器 }; const UI_TEXT = { panelTitle: 'YouTube捐款统计', toggleButton: '捐款统计', startButton: '开始统计', stopButton: '停止', exportButton: '导出数据', detailsButton: '查看详情', statusReady: '就绪 (请点击开始)', statusScanning: '正在扫描中...请手动滚动页面加载评论', noDataTip: '暂未检测到捐款', statsTitle: '捐款统计:', noStatsData: '暂无捐款数据', scannedComments: '已扫描评论: ', detectedDonations: '检测到捐款: ', donationDetailsTitle: '捐款详情', noDonationData: '暂无捐款数据', donationSummary: '总计: {total}笔捐款,来自{comments}条评论', donationTableHeaders: ['用户', '捐款金额', '评论内容'], exportNoData: '暂无数据可导出' }; // ========================= // 应用状态 // ========================= const AppState = { isScanning: false, // 是否正在扫描 totalProcessed: 0, // 已处理评论数 scanInterval: null, // 扫描定时器 scanStartTime: null, // 扫描开始时间 scanCount: 0, // 当前扫描次数 processedCommentIds: new Set(), // 已处理评论ID集合 // 统计数据 statistics: { totalDonations: 0, // 总捐款数 totalComments: 0, // 总评论数 currencyStats: {}, // 按币种统计: { "US$": { count: 0, total: 0, originalTexts: [] }, ... } donationComments: [] // 包含捐款的评论列表 }, // 重置统计数据 resetStats() { this.statistics = { totalDonations: 0, totalComments: 0, currencyStats: {}, donationComments: [] }; return this.statistics; }, // 更新状态 updateState(changes) { Object.assign(this, changes); } }; // 初始化状态 AppState.resetStats(); // ========================= // 数据模型 // ========================= /** * 评论数据模型 * @typedef {Object} CommentData * @property {string} id - 评论ID * @property {string} author - 评论作者 * @property {string} content - 评论内容 * @property {Array<DonationData>} donations - 捐款数据列表 */ /** * 捐款数据模型 * @typedef {Object} DonationData * @property {string} currencySymbol - 币种符号(原始文本格式) * @property {string} amount - 金额(原始文本格式) * @property {string} rawText - 原始完整文本 */ /** * 创建评论数据对象 * @param {Element} commentElement - 评论DOM元素 * @returns {CommentData} */ function createCommentData(commentElement) { // 生成唯一ID const id = 'comment_' + Math.random().toString(36).substr(2, 9); // 获取作者信息 const authorElement = commentElement.querySelector(SELECTORS.authorText); const author = authorElement ? authorElement.textContent.trim() : '未知用户'; // 获取评论内容 const contentElement = commentElement.querySelector(SELECTORS.commentContent); const content = contentElement ? contentElement.textContent.trim() : ''; return { id, author, content, donations: [] // 初始为空数组,后续处理时添加 }; } // ========================= // 捐款提取模块 // ========================= /** * 从捐款文本中提取币种和金额 * @param {string} text - 捐款文本 (如 "US$199.99", "JP¥10,000", "€20.00" 等) * @returns {Object|null} 包含currencySymbol和amount的对象,或null */ function parseDonationText(text) { // 清理文本 const cleanText = text.trim(); // 使用正则表达式找出数字部分(包括逗号和小数点) const numberMatch = /([\d,]+(\.\d{1,2})?)/g.exec(cleanText); if (!numberMatch) { return null; // 没有找到数字,不是有效的捐款格式 } // 提取数字部分(保持原始格式,包括逗号) const amount = numberMatch[0]; // 提取币种部分(数字之前的所有非空白字符) const startPos = cleanText.indexOf(amount); const currencySymbol = startPos > 0 ? cleanText.substring(0, startPos).trim() : ''; // 如果没有币种标识,则不是有效捐款 if (!currencySymbol) { return null; } return { currencySymbol, amount, rawText: cleanText }; } /** * 从评论中查找并处理捐款信息 * @param {Element} commentElement - 评论DOM元素 * @returns {Array} 捐款数据数组 */ function extractDonationsFromComment(commentElement) { const donations = []; // 直接查找捐款价格元素 - 先尝试精确选择器 let priceElements = commentElement.querySelectorAll(SELECTORS.donationChipPrice); // 如果没有找到,尝试使用更广泛的选择器 if (!priceElements || priceElements.length === 0) { priceElements = commentElement.querySelectorAll(SELECTORS.priceElements); } if (priceElements && priceElements.length > 0) { // 处理每个价格元素 priceElements.forEach(priceElement => { const priceText = priceElement.textContent.trim(); // 解析捐款文本获取币种和金额 const donationInfo = parseDonationText(priceText); if (donationInfo) { donations.push({ currencySymbol: donationInfo.currencySymbol, amount: donationInfo.amount, rawText: donationInfo.rawText }); log(`检测到捐款: ${donationInfo.currencySymbol} ${donationInfo.amount}`, priceText); } }); } return donations; } /** * 分析评论内容并更新统计 * @param {CommentData} commentData - 评论数据 * @param {Element} commentElement - 评论DOM元素 * @returns {boolean} 是否检测到捐款 */ function analyzeComment(commentData, commentElement) { // 从评论DOM中提取捐款信息 const donations = extractDonationsFromComment(commentElement); // 如果没有检测到捐款,返回false if (donations.length === 0) { return false; } // 将捐款信息添加到评论数据 commentData.donations = donations; // 更新统计数据 updateDonationStats(commentData); return true; } /** * 更新捐款统计数据 * @param {CommentData} commentData - 包含捐款信息的评论数据 */ function updateDonationStats(commentData) { const { statistics } = AppState; // 如果评论包含捐款,添加到捐款评论列表 if (commentData.donations.length > 0) { statistics.donationComments.push(commentData); statistics.totalDonations += commentData.donations.length; // 按币种更新统计 commentData.donations.forEach(donation => { const { currencySymbol, amount, rawText } = donation; // 如果这个币种还没有统计记录,创建一个 if (!statistics.currencyStats[currencySymbol]) { statistics.currencyStats[currencySymbol] = { count: 0, total: 0, originalTexts: [] }; } // 更新统计 const currencyStat = statistics.currencyStats[currencySymbol]; currencyStat.count++; currencyStat.originalTexts.push(rawText); // 提取数值部分用于累加(去除逗号) const numericAmount = parseFloat(amount.replace(/,/g, '')); if (!isNaN(numericAmount)) { currencyStat.total += numericAmount; } }); } } /** * 格式化捐款金额显示 * @param {string} currencySymbol - 币种符号 * @param {string} amount - 金额 * @returns {string} 格式化后的金额字符串 */ function formatDonation(currencySymbol, amount) { return `${currencySymbol}${amount}`; } // ========================= // 工具函数 // ========================= /** * 调试日志输出 * @param {string} message - 日志消息 * @param {any} data - 相关数据(可选) */ function log(message, data) { if (CONFIG.debug) { if (data !== undefined) { console.log(`[YT捐款统计] ${message}`, data); } else { console.log(`[YT捐款统计] ${message}`); } } } /** * 检查当前是否为YouTube视频页面 * @returns {boolean} */ function isYouTubeVideoPage() { return window.location.href.includes('youtube.com/watch'); } /** * 等待元素出现在DOM中 * @param {string} selector - CSS选择器 * @param {number} timeout - 超时时间(毫秒) * @returns {Promise<Element>} */ function waitForElement(selector, timeout = 30000) { return new Promise((resolve, reject) => { const element = document.querySelector(selector); if (element) { return resolve(element); } const observer = new MutationObserver((mutations, observer) => { const element = document.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); // 设置超时 setTimeout(() => { observer.disconnect(); reject(new Error(`等待元素 ${selector} 超时`)); }, timeout); }); } /** * 防抖函数 * @param {Function} func - 要执行的函数 * @param {number} wait - 等待时间(毫秒) * @returns {Function} 防抖处理后的函数 */ function debounce(func, wait = 300) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * 节流函数 * @param {Function} func - 要执行的函数 * @param {number} limit - 限制时间(毫秒) * @returns {Function} 节流处理后的函数 */ function throttle(func, limit = 300) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } /** * 错误处理器 * @param {Error} error - 错误对象 * @param {string} context - 错误上下文 */ function handleError(error, context) { log(`错误 [${context}]:`, error); // 在UI上显示错误 const status = document.getElementById('yt-donation-status'); if (status) { status.textContent = `发生错误: ${error.message}`; status.style.color = '#f44336'; } // 如果正在扫描,尝试恢复 if (AppState.isScanning) { log('尝试恢复扫描...'); // 短暂延迟后尝试继续 setTimeout(() => { if (AppState.isScanning) { processNewComments(); } }, 2000); } } /** * 等待评论区加载完成 * @returns {Promise<Element>} */ async function waitForComments() { log('等待评论区加载...'); try { const commentsSection = await waitForElement(SELECTORS.commentsSection); log('评论区已加载'); return commentsSection; } catch (error) { log('评论区加载失败', error); throw error; } } /** * 监听页面URL变化 * @param {Function} callback - URL变化时的回调函数 */ function watchPageChanges(callback) { let lastUrl = location.href; // 创建MutationObserver监听URL变化 const observer = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; callback(location.href); } }); // 开始观察 observer.observe(document.querySelector('body'), { childList: true, subtree: true }); return observer; } // ========================= // DOM分析相关函数 // ========================= /** * 初始化评论区监视器 * @param {Element} commentsSection - 评论区容器元素 */ function initObservers(commentsSection) { log('初始化评论区监视器'); // 创建MutationObserver监听评论区变化 const commentObserver = new MutationObserver((mutations) => { if (AppState.isScanning) { // 如果正在扫描中,检查新增评论 const newComments = getUnprocessedComments(); if (newComments.length > 0) { log(`检测到${newComments.length}条新评论`); // 重置无新评论计数 AppState.noNewCommentsCount = 0; } } }); // 开始观察评论区变化 commentObserver.observe(commentsSection, { childList: true, subtree: true }); log('评论区监视器已初始化'); } /** * 获取未处理的评论元素 * @returns {NodeList} */ function getUnprocessedComments() { // 获取所有评论元素 const allComments = document.querySelectorAll(SELECTORS.commentThreads); // 过滤出未处理的评论 return Array.from(allComments).filter(comment => { // 为每个评论生成一个唯一ID const commentId = generateCommentId(comment); // 检查是否已经处理过 return !AppState.processedCommentIds.has(commentId); }); } /** * 为评论生成唯一ID * @param {Element} commentElement - 评论DOM元素 * @returns {string} 唯一ID */ function generateCommentId(commentElement) { // 获取作者和内容作为ID的组成部分 const authorElement = commentElement.querySelector(SELECTORS.authorText); const contentElement = commentElement.querySelector(SELECTORS.commentContent); const author = authorElement ? authorElement.textContent.trim() : ''; const content = contentElement ? contentElement.textContent.trim() : ''; // 组合作者和内容的简短摘要作为ID return `${author.slice(0, 20)}_${content.slice(0, 30)}`; } /** * 处理所有未处理的评论 * @returns {number} 处理的评论数量 */ function processNewComments() { // 获取所有未处理评论 const newComments = getUnprocessedComments(); let processedCount = 0; if (newComments.length === 0) { log(`未找到新评论,已处理总数: ${AppState.totalProcessed}`); return 0; } // 处理每条评论 newComments.forEach(commentElement => { try { // 创建评论数据 const commentData = createCommentData(commentElement); // 生成并记录评论ID const commentId = generateCommentId(commentElement); AppState.processedCommentIds.add(commentId); // 更新统计 AppState.statistics.totalComments++; AppState.totalProcessed++; // 分析评论 analyzeComment(commentData, commentElement); processedCount++; } catch (error) { log('处理评论时出错', error); } }); if (processedCount > 0) { log(`已处理${processedCount}条新评论,累计: ${AppState.totalProcessed}`); updateUI(); // 更新UI显示 } return processedCount; } /** * 开始扫描评论 */ function startScanning() { if (AppState.isScanning) { log('已经在扫描中'); return; } log('开始扫描评论'); // 更新状态 AppState.updateState({ isScanning: true, scanStartTime: Date.now(), scanCount: 0, totalProcessed: 0, processedCommentIds: new Set() }); // 重置统计 AppState.resetStats(); // 初始扫描一次 processNewComments(); // 设置定时扫描 AppState.scanInterval = setInterval(() => { // 更新扫描计数 AppState.scanCount++; // 处理新评论 processNewComments(); // 更新状态显示 updateUIState(); }, CONFIG.scanInterval); // 更新UI状态 updateUIState(); } /** * 停止扫描评论 */ function stopScanning() { if (!AppState.isScanning) { return; } log('停止扫描评论'); // 清除定时器 if (AppState.scanInterval) { clearInterval(AppState.scanInterval); AppState.scanInterval = null; } // 更新状态 AppState.updateState({ isScanning: false }); // 更新UI状态 updateUIState(); // 显示最终统计结果 log('扫描完成,最终统计', AppState.statistics); } // ========================= // UI相关函数 // ========================= /** * 创建UI控制面板 */ function createUI() { log('创建UI控制面板'); // 创建主容器 const panel = document.createElement('div'); panel.id = 'yt-donation-tracker-panel'; panel.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 280px; background: white; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 9999; font-family: Arial, sans-serif; padding: 12px; color: #333; display: none; `; // 创建标题 const title = document.createElement('div'); title.style.cssText = ` font-weight: bold; font-size: 14px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; `; title.textContent = UI_TEXT.panelTitle; // 添加关闭按钮 const closeBtn = document.createElement('span'); closeBtn.textContent = '×'; closeBtn.style.cssText = ` cursor: pointer; padding: 0 5px; font-size: 16px; `; closeBtn.addEventListener('click', () => { panel.style.display = 'none'; toggleButton.style.display = 'block'; }); title.appendChild(closeBtn); // 创建内容容器 const content = document.createElement('div'); content.id = 'yt-donation-tracker-content'; // 创建按钮容器 const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 8px; margin-bottom: 10px; `; // 创建操作按钮(开始/停止) const actionBtn = document.createElement('button'); actionBtn.id = 'yt-donation-action-btn'; actionBtn.textContent = UI_TEXT.startButton; actionBtn.style.cssText = buttonStyle(); actionBtn.addEventListener('click', () => { if (AppState.isScanning) { stopScanning(); } else { startScanning(); } }); // 添加按钮到容器 buttonContainer.appendChild(actionBtn); // 创建状态显示 const status = document.createElement('div'); status.id = 'yt-donation-status'; status.style.cssText = ` font-size: 12px; margin: 8px 0; color: #666; `; status.textContent = UI_TEXT.statusReady; // 创建统计结果容器 const results = document.createElement('div'); results.id = 'yt-donation-results'; results.style.cssText = ` margin-top: 10px; font-size: 13px; `; // 创建导出按钮 const exportBtn = document.createElement('button'); exportBtn.id = 'yt-donation-export-btn'; exportBtn.textContent = UI_TEXT.exportButton; exportBtn.style.cssText = buttonStyle('#2196f3'); exportBtn.style.display = 'none'; exportBtn.addEventListener('click', exportData); // 创建详情按钮 const detailsBtn = document.createElement('button'); detailsBtn.id = 'yt-donation-details-btn'; detailsBtn.textContent = UI_TEXT.detailsButton; detailsBtn.style.cssText = buttonStyle('#ff9800'); detailsBtn.style.display = 'none'; detailsBtn.addEventListener('click', showDetailsModal); // 创建操作按钮容器 const actionContainer = document.createElement('div'); actionContainer.style.cssText = ` display: flex; gap: 8px; margin-top: 10px; `; actionContainer.appendChild(exportBtn); actionContainer.appendChild(detailsBtn); // 组装UI content.appendChild(buttonContainer); content.appendChild(status); content.appendChild(results); content.appendChild(actionContainer); panel.appendChild(title); panel.appendChild(content); // 创建侧边呼出按钮 const toggleButton = document.createElement('div'); toggleButton.id = 'yt-donation-toggle'; toggleButton.textContent = UI_TEXT.toggleButton; toggleButton.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #4caf50; color: white; padding: 8px 16px; font-size: 13px; border-radius: 4px; cursor: pointer; z-index: 9998; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); text-align: center; font-weight: bold; `; toggleButton.addEventListener('click', () => { panel.style.display = 'block'; toggleButton.style.display = 'none'; }); // 添加到页面 document.body.appendChild(panel); document.body.appendChild(toggleButton); // 初始化UI updateUI(); log('UI控制面板已创建'); return { panel, toggleButton }; } /** * 按钮样式生成器 * @param {string} bgColor - 背景颜色 * @returns {string} CSS样式字符串 */ function buttonStyle(bgColor = '#4caf50') { return ` padding: 6px 12px; border: none; border-radius: 4px; background: ${bgColor}; color: white; font-size: 12px; cursor: pointer; flex: 1; `; } /** * 更新UI显示 */ function updateUI() { const results = document.getElementById('yt-donation-results'); if (!results) return; const { statistics } = AppState; // 清空结果容器 while (results.firstChild) { results.removeChild(results.firstChild); } // 创建扫描计数元素 const scanCountDiv = document.createElement('div'); scanCountDiv.style.marginBottom = '5px'; scanCountDiv.appendChild(document.createTextNode(UI_TEXT.scannedComments)); const scanCountStrong = document.createElement('strong'); scanCountStrong.textContent = statistics.totalComments; scanCountDiv.appendChild(scanCountStrong); results.appendChild(scanCountDiv); // 创建捐款计数元素 const donationCountDiv = document.createElement('div'); donationCountDiv.style.marginBottom = '8px'; donationCountDiv.appendChild(document.createTextNode(UI_TEXT.detectedDonations)); const donationCountStrong = document.createElement('strong'); donationCountStrong.textContent = statistics.totalDonations; donationCountDiv.appendChild(donationCountStrong); results.appendChild(donationCountDiv); // 没有捐款时显示提示 if (statistics.totalDonations === 0) { const noDataDiv = document.createElement('div'); noDataDiv.style.color = '#666'; noDataDiv.style.fontStyle = 'italic'; noDataDiv.textContent = UI_TEXT.noDataTip; results.appendChild(noDataDiv); } else { // 创建捐款统计标题 const statsTitle = document.createElement('div'); statsTitle.style.fontWeight = 'bold'; statsTitle.style.marginTop = '5px'; statsTitle.textContent = UI_TEXT.statsTitle; results.appendChild(statsTitle); // 创建列表 const statsList = document.createElement('ul'); statsList.style.margin = '5px 0'; statsList.style.paddingLeft = '20px'; let hasDonations = false; // 遍历所有币种统计 Object.keys(statistics.currencyStats).forEach(symbol => { const stat = statistics.currencyStats[symbol]; // 只显示有捐款的币种 if (stat.count > 0) { hasDonations = true; const listItem = document.createElement('li'); // 添加币种信息 listItem.appendChild(document.createTextNode(`${symbol}: `)); // 添加金额 const amountStrong = document.createElement('strong'); // 显示两位小数,除非是整数 let totalDisplay = stat.total; if (Math.floor(stat.total) !== stat.total) { totalDisplay = stat.total.toFixed(2); } amountStrong.textContent = totalDisplay; listItem.appendChild(amountStrong); // 添加笔数 listItem.appendChild(document.createTextNode(` (${stat.count}笔)`)); statsList.appendChild(listItem); } }); if (!hasDonations) { const listItem = document.createElement('li'); listItem.textContent = UI_TEXT.noStatsData; statsList.appendChild(listItem); } results.appendChild(statsList); // 显示操作按钮 const exportBtn = document.getElementById('yt-donation-export-btn'); const detailsBtn = document.getElementById('yt-donation-details-btn'); if (exportBtn) exportBtn.style.display = 'block'; if (detailsBtn) detailsBtn.style.display = 'block'; } } /** * 更新UI状态 */ function updateUIState() { // 更新按钮状态 const actionBtn = document.getElementById('yt-donation-action-btn'); const panel = document.getElementById('yt-donation-tracker-panel'); const toggleButton = document.getElementById('yt-donation-toggle'); if (actionBtn) { if (AppState.isScanning) { actionBtn.textContent = UI_TEXT.stopButton; actionBtn.style.background = '#f44336'; // 红色 } else { actionBtn.textContent = UI_TEXT.startButton; actionBtn.style.background = '#4caf50'; // 绿色 } } // 如果正在扫描,确保面板显示 if (AppState.isScanning && panel && toggleButton) { panel.style.display = 'block'; toggleButton.style.display = 'none'; } // 更新状态文本 const status = document.getElementById('yt-donation-status'); if (status) { if (AppState.isScanning) { const scannedTime = Math.round((Date.now() - AppState.scanStartTime) / 1000); status.textContent = UI_TEXT.statusScanning .replace('{count}', AppState.totalProcessed) .replace('{time}', scannedTime); } else { status.textContent = UI_TEXT.statusReady; } } } /** * 创建详情模态窗口 */ function showDetailsModal() { // 移除已有的模态窗口 const existingModal = document.getElementById('yt-donation-details-modal'); if (existingModal) { existingModal.remove(); } const { statistics } = AppState; // 创建模态窗口容器 const modal = document.createElement('div'); modal.id = 'yt-donation-details-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; `; // 添加点击空白处关闭功能 modal.addEventListener('click', (event) => { // 如果点击的是模态窗口背景(而不是内容区域),关闭窗口 if (event.target === modal) { modal.remove(); } }); // 创建模态窗口内容 const modalContent = document.createElement('div'); modalContent.style.cssText = ` background: white; border-radius: 8px; max-width: 800px; width: 80%; max-height: 80vh; overflow-y: auto; padding: 20px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); `; // 创建标题和关闭按钮 const titleRow = document.createElement('div'); titleRow.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; `; const title = document.createElement('h2'); title.style.cssText = ` margin: 0; font-size: 18px; `; title.textContent = UI_TEXT.donationDetailsTitle; const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.cssText = ` background: none; border: none; font-size: 24px; cursor: pointer; color: #666; padding: 0 5px; `; closeBtn.addEventListener('click', () => modal.remove()); titleRow.appendChild(title); titleRow.appendChild(closeBtn); // 创建内容 const contentDiv = document.createElement('div'); // 构建捐款详情 if (statistics.donationComments.length === 0) { const noDataP = document.createElement('p'); noDataP.textContent = UI_TEXT.noDonationData; contentDiv.appendChild(noDataP); } else { // 创建总计信息 const summaryDiv = document.createElement('div'); summaryDiv.style.marginBottom = '15px'; const summaryStrong = document.createElement('strong'); summaryStrong.textContent = UI_TEXT.donationSummary .replace('{total}', statistics.totalDonations) .replace('{comments}', statistics.donationComments.length); summaryDiv.appendChild(summaryStrong); contentDiv.appendChild(summaryDiv); // 创建表格 const table = document.createElement('table'); table.style.width = '100%'; table.style.borderCollapse = 'collapse'; // 创建表头 const thead = document.createElement('thead'); const headerRow = document.createElement('tr'); const headers = UI_TEXT.donationTableHeaders; headers.forEach(headerText => { const th = document.createElement('th'); th.style.textAlign = 'left'; th.style.padding = '8px'; th.style.borderBottom = '1px solid #ddd'; th.textContent = headerText; headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); // 创建表格内容 const tbody = document.createElement('tbody'); // 添加每条捐款评论 statistics.donationComments.forEach(comment => { const row = document.createElement('tr'); // 用户单元格 const userCell = document.createElement('td'); userCell.style.padding = '8px'; userCell.style.borderBottom = '1px solid #eee'; userCell.textContent = comment.author; row.appendChild(userCell); // 捐款金额单元格 const donationCell = document.createElement('td'); donationCell.style.padding = '8px'; donationCell.style.borderBottom = '1px solid #eee'; // 构建该评论的所有捐款文本 const donationsText = comment.donations.map(d => formatDonation(d.currencySymbol, d.amount) ).join(', '); donationCell.textContent = donationsText; row.appendChild(donationCell); // 评论内容单元格 const contentCell = document.createElement('td'); contentCell.style.padding = '8px'; contentCell.style.borderBottom = '1px solid #eee'; contentCell.textContent = comment.content; row.appendChild(contentCell); tbody.appendChild(row); }); table.appendChild(tbody); contentDiv.appendChild(table); } // 组装模态窗口 modalContent.appendChild(titleRow); modalContent.appendChild(contentDiv); modal.appendChild(modalContent); // 添加到页面 document.body.appendChild(modal); } /** * 导出数据为CSV文件 */ function exportData() { const { statistics } = AppState; if (statistics.donationComments.length === 0) { alert(UI_TEXT.exportNoData); return; } // 创建CSV内容 let csvContent = '用户,捐款金额,货币,评论内容\n'; statistics.donationComments.forEach(comment => { comment.donations.forEach(donation => { // 格式化CSV行 const row = [ csvEscape(comment.author), donation.amount, donation.currencySymbol, csvEscape(comment.content) ]; csvContent += row.join(',') + '\n'; }); }); // 使用Blob创建CSV文件 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); // 创建下载链接 const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', `youtube_donations_${Date.now()}.csv`); link.style.display = 'none'; document.body.appendChild(link); // 触发下载 link.click(); // 清理 document.body.removeChild(link); URL.revokeObjectURL(url); // 释放URL对象 } /** * 转义CSV中的文本 * @param {string} text - 原始文本 * @returns {string} 转义后的文本 */ function csvEscape(text) { // 如果文本中包含逗号、引号或换行符,需要用引号包裹 if (/[",\n\r]/.test(text)) { // 将文本中的引号替换为两个引号 return '"' + text.replace(/"/g, '""') + '"'; } return text; } /** * 转义HTML特殊字符 * @param {string} text - 原始文本 * @returns {string} 转义后的文本 */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ========================= // 主逻辑 // ========================= /** * 脚本初始化 */ async function init() { log('YouTube捐款统计器初始化...'); // 检查是否在YouTube视频页面 if (!isYouTubeVideoPage()) { log('不是YouTube视频页面,等待页面变化'); // 监听页面变化 watchPageChanges(url => { if (url.includes('youtube.com/watch')) { log('检测到视频页面,开始初始化'); initOnVideoPage(); } }); return; } await initOnVideoPage(); } /** * 在视频页面上初始化 */ async function initOnVideoPage() { try { // 清理旧的UI和状态 cleanup(); // 等待评论区加载 const commentsSection = await waitForComments(); // 创建UI const { panel, toggleButton } = createUI(); // 初始化观察器 initObservers(commentsSection); log('初始化完成'); } catch (error) { handleError(error, '初始化'); // 如果是评论区加载失败,可能是YouTube的SPA导航,尝试延迟重试 setTimeout(() => { if (isYouTubeVideoPage()) { log('尝试重新初始化...'); initOnVideoPage(); } }, 3000); } } /** * 清理旧的UI和状态 */ function cleanup() { log('清理旧状态...'); // 停止扫描 stopScanning(); // 移除旧UI const oldPanel = document.getElementById('yt-donation-tracker-panel'); if (oldPanel) { oldPanel.remove(); } // 移除旧的模态窗口 const oldModal = document.getElementById('yt-donation-details-modal'); if (oldModal) { oldModal.remove(); } // 重置状态 AppState.resetStats(); AppState.updateState({ isScanning: false, totalProcessed: 0, scanInterval: null, scanStartTime: null, scanCount: 0, processedCommentIds: new Set() }); } // 页面加载完成后执行初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址