From c01cc11b76fe6f0bb75ec196011734783b57393a Mon Sep 17 00:00:00 2001 From: Michael Kane <15847202+jhhd88@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:41:19 +0800 Subject: [PATCH 1/3] Update LDStatus.user.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. DOM 操作优化: - 使用 DocumentFragment 进行批量 DOM 操作,减少重绘 - 用直接创建元素替代 innerHTML,提高安全性和性能 - 添加了 clearContent 函数清空内容区域 2. 内存和存储优化: - 为每日统计数据实施存储限制(MAX_ENTRIES_PER_STAT = 50) - 清理超过24小时的旧数据 3. 性能优化: - 添加拖动操作节流函数(throttle) - 使用 requestAnimationFrame 实现更流畅的拖动动画 4. 数据处理优化: - 使用更高效的选择器查找信任级别section - 将大函数拆分成专注的小函数:findTrustLevelSection, extractUserInfo, checkRequirementStatus, extractRequirements 等 6. 代码结构改进: - 重构了 parseTrustLevelData 函数,拆分为多个专注的小函数 - 提高了代码的可维护性和可读性 7. 错误处理增强: - 添加了完整的错误处理流程 - 为 GM_xmlhttpRequest 添加了超时处理 - 添加了错误信息显示函数 8. 资源使用优化: - 实现了闲置检测功能,页面不可见时停止自动刷新 - 使用 setupRefreshInterval 管理刷新间隔 --- LDStatus.user.js | 553 +++++++++++++++++++++++++++++++---------------- 1 file changed, 364 insertions(+), 189 deletions(-) diff --git a/LDStatus.user.js b/LDStatus.user.js index 3ba9164..a6b11f4 100644 --- a/LDStatus.user.js +++ b/LDStatus.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name LDStatus // @namespace http://tampermonkey.net/ -// @version 1.7 +// @version 1.8 // @description 在 Linux.do 页面显示信任级别进度 // @author 1e0n // @match https://linux.do/* @@ -210,6 +210,7 @@ // 定义存储键 const STORAGE_KEY_POSITION = 'ld_panel_position'; const STORAGE_KEY_COLLAPSED = 'ld_panel_collapsed'; + const STORAGE_KEY_LAST_UPDATE_CHECK = 'ld_last_update_check'; // 创建面板 const panel = document.createElement('div'); @@ -221,26 +222,73 @@ // 创建面板头部 const header = document.createElement('div'); header.id = 'ld-trust-level-header'; - header.innerHTML = ` -
- Status - v${scriptVersion} - - - -
- `; + + const headerContent = document.createElement('div'); + headerContent.className = 'ld-header-content'; + + const statusSpan = document.createElement('span'); + statusSpan.textContent = 'Status'; + headerContent.appendChild(statusSpan); + + const versionSpan = document.createElement('span'); + versionSpan.className = 'ld-version'; + versionSpan.textContent = `v${scriptVersion}`; + headerContent.appendChild(versionSpan); + + const updateBtn = document.createElement('button'); + updateBtn.className = 'ld-update-btn'; + updateBtn.title = '检查更新'; + updateBtn.textContent = '🔎'; + headerContent.appendChild(updateBtn); + + const refreshBtn = document.createElement('button'); + refreshBtn.className = 'ld-refresh-btn'; + refreshBtn.title = '刷新数据'; + refreshBtn.textContent = '🔄'; + headerContent.appendChild(refreshBtn); + + const toggleBtn = document.createElement('button'); + toggleBtn.className = 'ld-toggle-btn'; + toggleBtn.title = '展开/收起'; + toggleBtn.textContent = '◀'; + headerContent.appendChild(toggleBtn); + + header.appendChild(headerContent); // 创建内容区域 const content = document.createElement('div'); content.id = 'ld-trust-level-content'; - content.innerHTML = '
加载中...
'; - + // 组装面板 panel.appendChild(header); panel.appendChild(content); document.body.appendChild(panel); + // 显示加载信息的函数 + function showLoading() { + clearContent(); + const loadingDiv = document.createElement('div'); + loadingDiv.className = 'ld-loading'; + loadingDiv.textContent = '加载中...'; + content.appendChild(loadingDiv); + } + + // 显示错误信息的函数 + function showErrorMessage(message) { + clearContent(); + const errorDiv = document.createElement('div'); + errorDiv.className = 'ld-loading'; + errorDiv.textContent = message; + content.appendChild(errorDiv); + } + + // 清空内容区域的函数 + function clearContent() { + while (content.firstChild) { + content.removeChild(content.firstChild); + } + } + // 保存窗口位置的函数 function savePanelPosition() { const transform = window.getComputedStyle(panel).transform; @@ -274,6 +322,20 @@ } } + // 实现节流函数 + function throttle(func, limit) { + let inThrottle; + return function() { + const args = arguments; + const context = this; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + // 拖动功能 let isDragging = false; let lastX, lastY; @@ -290,24 +352,22 @@ document.body.style.userSelect = 'none'; }); - document.addEventListener('mousemove', (e) => { + document.addEventListener('mousemove', throttle((e) => { if (!isDragging) return; - // 使用 transform 而不是改变 left/top 属性,性能更好 const dx = e.clientX - lastX; const dy = e.clientY - lastY; - - const currentTransform = window.getComputedStyle(panel).transform; - const matrix = new DOMMatrix(currentTransform === 'none' ? '' : currentTransform); - - const newX = matrix.e + dx; - const newY = matrix.f + dy; - - panel.style.transform = `translate(${newX}px, ${newY}px)`; - lastX = e.clientX; lastY = e.clientY; - }); + + requestAnimationFrame(() => { + const currentTransform = window.getComputedStyle(panel).transform; + const matrix = new DOMMatrix(currentTransform === 'none' ? '' : currentTransform); + const newX = matrix.e + dx; + const newY = matrix.f + dy; + panel.style.transform = `translate(${newX}px, ${newY}px)`; + }); + }, 16)); document.addEventListener('mouseup', () => { if (!isDragging) return; @@ -321,7 +381,6 @@ }); // 展开/收起功能 - const toggleBtn = header.querySelector('.ld-toggle-btn'); toggleBtn.addEventListener('click', () => { panel.classList.toggle('ld-collapsed'); toggleBtn.textContent = panel.classList.contains('ld-collapsed') ? '▶' : '◀'; @@ -331,15 +390,27 @@ }); // 刷新按钮 - const refreshBtn = header.querySelector('.ld-refresh-btn'); refreshBtn.addEventListener('click', fetchTrustLevelData); // 检查更新按钮 - const updateBtn = header.querySelector('.ld-update-btn'); updateBtn.addEventListener('click', checkForUpdates); // 检查脚本更新 function checkForUpdates() { + const lastCheck = GM_getValue(STORAGE_KEY_LAST_UPDATE_CHECK, 0); + const now = Date.now(); + + // 一天只检查一次 + if (now - lastCheck < 86400000) { + updateBtn.textContent = '⏱️'; + updateBtn.title = '今天已检查过更新'; + setTimeout(() => { + updateBtn.textContent = '🔎'; + updateBtn.title = '检查更新'; + }, 2000); + return; + } + const updateURL = 'https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js'; // 显示正在检查的状态 @@ -349,50 +420,64 @@ GM_xmlhttpRequest({ method: 'GET', url: updateURL, + timeout: 10000, // 添加超时设置 onload: function(response) { if (response.status === 200) { - // 提取远程脚本的版本号 - const versionMatch = response.responseText.match(/@version\s+([\d\.]+)/); - if (versionMatch && versionMatch[1]) { - const remoteVersion = versionMatch[1]; - - // 比较版本 - if (remoteVersion > scriptVersion) { - // 有新版本 - updateBtn.textContent = '⚠️'; // 警告图标 - updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`; - updateBtn.style.color = '#ffd700'; // 黄色 - - // 点击按钮跳转到更新页面 - updateBtn.onclick = function() { - window.open(updateURL, '_blank'); - }; + try { + // 提取远程脚本的版本号 + const versionMatch = response.responseText.match(/@version\s+([\d\.]+)/); + if (versionMatch && versionMatch[1]) { + const remoteVersion = versionMatch[1]; + + // 比较版本 + if (remoteVersion > scriptVersion) { + // 有新版本 + updateBtn.textContent = '⚠️'; // 警告图标 + updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`; + updateBtn.style.color = '#ffd700'; // 黄色 + + // 点击按钮跳转到更新页面 + updateBtn.onclick = function() { + window.open(updateURL, '_blank'); + }; + } else { + // 已是最新版本 + updateBtn.textContent = '✔'; // 勾选图标 + updateBtn.title = '已是最新版本'; + updateBtn.style.color = '#68d391'; // 绿色 + + // 3秒后恢复原样式 + setTimeout(() => { + updateBtn.textContent = '🔎'; // 放大镜图标 + updateBtn.title = '检查更新'; + updateBtn.style.color = 'white'; + updateBtn.onclick = checkForUpdates; + }, 3000); + } } else { - // 已是最新版本 - updateBtn.textContent = '✔'; // 勾选图标 - updateBtn.title = '已是最新版本'; - updateBtn.style.color = '#68d391'; // 绿色 - - // 3秒后恢复原样式 - setTimeout(() => { - updateBtn.textContent = '🔎'; // 放大镜图标 - updateBtn.title = '检查更新'; - updateBtn.style.color = 'white'; - updateBtn.onclick = checkForUpdates; - }, 3000); + handleUpdateError('无法解析版本信息'); } - } else { - handleUpdateError(); + } catch (error) { + handleUpdateError('处理更新信息时出错: ' + error.message); } } else { - handleUpdateError(); + handleUpdateError(`请求失败 (${response.status})`); } + + // 更新检查时间 + GM_setValue(STORAGE_KEY_LAST_UPDATE_CHECK, now); }, - onerror: handleUpdateError + onerror: function(error) { + handleUpdateError('网络请求失败'); + }, + ontimeout: function() { + handleUpdateError('请求超时'); + } }); // 处理更新检查错误 - function handleUpdateError() { + function handleUpdateError(message) { + console.error('检查更新失败:', message); updateBtn.textContent = '❌'; // 错误图标 updateBtn.title = '检查更新失败,请稍后再试'; updateBtn.style.color = '#fc8181'; // 红色 @@ -408,48 +493,60 @@ // 获取信任级别数据 function fetchTrustLevelData() { - content.innerHTML = '
加载中...
'; + showLoading(); GM_xmlhttpRequest({ method: 'GET', url: 'https://connect.linux.do', + timeout: 10000, // 添加超时设置 onload: function(response) { if (response.status === 200) { - parseTrustLevelData(response.responseText); + try { + parseTrustLevelData(response.responseText); + } catch (error) { + console.error('解析数据时出错:', error); + showErrorMessage('解析数据时出错: ' + error.message); + } } else { - content.innerHTML = '
获取数据失败,请稍后再试
'; + showErrorMessage(`获取数据失败 (${response.status})`); } }, - onerror: function() { - content.innerHTML = '
获取数据失败,请稍后再试
'; + onerror: function(error) { + console.error('请求错误:', error); + showErrorMessage('网络请求失败'); + }, + ontimeout: function() { + showErrorMessage('请求超时'); } }); } - // 解析信任级别数据 - function parseTrustLevelData(html) { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - // 查找信任级别区块 - const trustLevelSection = Array.from(doc.querySelectorAll('.bg-white.p-6.rounded-lg')).find(div => { - const heading = div.querySelector('h2'); - return heading && heading.textContent.includes('信任级别'); - }); - - if (!trustLevelSection) { - content.innerHTML = '
未找到信任级别数据,请确保已登录
'; - return; - } + // 查找信任级别区块 + function findTrustLevelSection(doc) { + const headers = doc.querySelectorAll('h2'); + const trustHeader = Array.from(headers).find(h => h.textContent.includes('信任级别')); + return trustHeader ? trustHeader.closest('.bg-white.p-6.rounded-lg') : null; + } - // 获取用户名和当前级别 - const heading = trustLevelSection.querySelector('h2').textContent.trim(); + // 提取用户信息 + function extractUserInfo(section) { + const heading = section.querySelector('h2').textContent.trim(); const match = heading.match(/(.*) - 信任级别 (\d+) 的要求/); - const username = match ? match[1] : '未知用户'; - const targetLevel = match ? match[2] : '未知'; + return { + username: match ? match[1] : '未知用户', + targetLevel: match ? match[2] : '未知' + }; + } - // 获取表格数据 - const tableRows = trustLevelSection.querySelectorAll('table tr'); + // 检查需求状态 + function checkRequirementStatus(section) { + const resultText = section.querySelector('p.text-red-500, p.text-green-500'); + return resultText ? !resultText.classList.contains('text-red-500') : false; + } + + // 提取需求数据 + function extractRequirements(section, previousRequirements) { + const tableRows = section.querySelectorAll('table tr'); const requirements = []; for (let i = 1; i < tableRows.length; i++) { // 跳过表头 @@ -497,9 +594,30 @@ } } + return requirements; + } + + // 解析信任级别数据 + function parseTrustLevelData(html) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + // 查找信任级别区块 + const trustLevelSection = findTrustLevelSection(doc); + + if (!trustLevelSection) { + showErrorMessage('未找到信任级别数据,请确保已登录'); + return; + } + + // 获取用户名和当前级别 + const { username, targetLevel } = extractUserInfo(trustLevelSection); + + // 获取表格数据 + const requirements = extractRequirements(trustLevelSection, previousRequirements); + // 获取总体结果 - const resultText = trustLevelSection.querySelector('p.text-red-500, p.text-green-500'); - const isMeetingRequirements = resultText ? !resultText.classList.contains('text-red-500') : false; + const isMeetingRequirements = checkRequirementStatus(trustLevelSection); // 存储24小时内的数据变化 const dailyChanges = saveDailyStats(requirements); @@ -513,15 +631,26 @@ // 渲染信任级别数据 function renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges = {}) { - let html = ` -
- ${username} - 信任级别 ${targetLevel} -
-
- ${isMeetingRequirements ? '已' : '未'}符合信任级别 ${targetLevel} 要求 -
- `; - + clearContent(); + + const fragment = document.createDocumentFragment(); + + // 创建用户和级别信息 + const headerDiv = document.createElement('div'); + headerDiv.style.marginBottom = '8px'; + headerDiv.style.fontWeight = 'bold'; + headerDiv.textContent = `${username} - 信任级别 ${targetLevel}`; + fragment.appendChild(headerDiv); + + // 创建需求状态信息 + const statusDiv = document.createElement('div'); + statusDiv.style.marginBottom = '10px'; + statusDiv.style.color = isMeetingRequirements ? '#68d391' : '#fc8181'; + statusDiv.style.fontSize = '11px'; + statusDiv.textContent = `${isMeetingRequirements ? '已' : '未'}符合信任级别 ${targetLevel} 要求`; + fragment.appendChild(statusDiv); + + // 创建需求列表 requirements.forEach(req => { // 简化项目名称 let name = req.name; @@ -543,32 +672,49 @@ if (currentMatch) current = currentMatch[1]; if (requiredMatch) required = requiredMatch[1]; - - // 添加目标完成数变化的标识 - let changeIndicator = ''; + + // 创建需求项 + const reqDiv = document.createElement('div'); + reqDiv.className = `ld-trust-level-item ${req.isSuccess ? 'ld-success' : 'ld-fail'}`; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'ld-name'; + nameSpan.textContent = name; + reqDiv.appendChild(nameSpan); + + const valueSpan = document.createElement('span'); + valueSpan.className = 'ld-value'; + + // 添加目标完成数 + valueSpan.textContent = `${current} / ${required}`; + + // 添加变化指示器 if (req.hasChanged) { + const changeIndicator = document.createElement('span'); const diff = req.changeValue; if (diff > 0) { - changeIndicator = ` ▲${diff}`; // 增加标识,黄色 + changeIndicator.className = 'ld-increase'; + changeIndicator.textContent = ` ▲${diff}`; } else if (diff < 0) { - changeIndicator = ` ▼${Math.abs(diff)}`; // 减少标识,蓝色 + changeIndicator.className = 'ld-decrease'; + changeIndicator.textContent = ` ▼${Math.abs(diff)}`; } + valueSpan.appendChild(changeIndicator); } - - html += ` -
- ${name} - ${current}${changeIndicator} / ${required} -
- `; + + reqDiv.appendChild(valueSpan); + fragment.appendChild(reqDiv); }); - - // 添加24小时内的活动数据显示 - html += ` -
-
24小时内的活动
- `; - + + // 创建24小时活动数据 + const dailyStatsDiv = document.createElement('div'); + dailyStatsDiv.className = 'ld-daily-stats'; + + const dailyStatsTitleDiv = document.createElement('div'); + dailyStatsTitleDiv.className = 'ld-daily-stats-title'; + dailyStatsTitleDiv.textContent = '24小时内的活动'; + dailyStatsDiv.appendChild(dailyStatsTitleDiv); + // 添加每个数据项 const dailyStatsItems = [ { name: '浏览话题', key: '浏览的话题(所有时间)' }, @@ -580,17 +726,25 @@ dailyStatsItems.forEach(item => { const value = dailyChanges[item.key] || 0; - html += ` -
- ${item.name} - ${value} -
- `; + + const statsItemDiv = document.createElement('div'); + statsItemDiv.className = 'ld-daily-stats-item'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'ld-name'; + nameSpan.textContent = item.name; + statsItemDiv.appendChild(nameSpan); + + const valueSpan = document.createElement('span'); + valueSpan.className = 'ld-value'; + valueSpan.textContent = value; + statsItemDiv.appendChild(valueSpan); + + dailyStatsDiv.appendChild(statsItemDiv); }); - - html += `
`; - - content.innerHTML = html; + + fragment.appendChild(dailyStatsDiv); + content.appendChild(fragment); } // 存储上一次获取的数据,用于比较变化 @@ -603,85 +757,106 @@ '浏览的话题(所有时间)', // 浏览话题总数 '回复的话题', // 回复话题数 '已读帖子(所有时间)', // 已读帖子总数 - '获赞:点赞用户数量', // 获赞数 - '点赞的帖子' // 点赞数 + '获赞:点赞用户数量', // 获得点赞 + '点赞的帖子' // 点赞帖子 ]; - // 获取当前时间 - const now = new Date().getTime(); + // 从存储中获取之前的记录 + let dailyStats = GM_getValue('ld_daily_stats', []); + + // 获取当前时间戳 + const now = Date.now(); - // 从 localStorage 中获取已存储的数据 - let dailyStats = JSON.parse(localStorage.getItem('ld_daily_stats') || '[]'); + // 清理超过24小时的旧数据 + dailyStats = dailyStats.filter(stat => now - stat.timestamp < 24 * 60 * 60 * 1000); - // 删除超过24小时的数据 - const oneDayAgo = now - 24 * 60 * 60 * 1000; - dailyStats = dailyStats.filter(item => item.timestamp > oneDayAgo); + // 提取要跟踪的数据项 + const trackedStats = requirements.filter(req => statsToTrack.includes(req.name)); - // 对于每个要跟踪的数据项,找到当前值并添加到历史记录中 + // 为每个要跟踪的项目添加新记录 + trackedStats.forEach(stat => { + dailyStats.push({ + name: stat.name, + value: stat.currentValue, + timestamp: now + }); + }); + + // 限制每种统计类型的条目数,防止过度存储 + const MAX_ENTRIES_PER_STAT = 50; statsToTrack.forEach(statName => { - const req = requirements.find(r => r.name === statName); - if (req) { - // 添加新的数据点 - dailyStats.push({ - name: statName, - value: req.currentValue, - timestamp: now - }); + const statEntries = dailyStats.filter(item => item.name === statName); + if (statEntries.length > MAX_ENTRIES_PER_STAT) { + // 只保留最新的 MAX_ENTRIES_PER_STAT 条记录 + const sortedEntries = statEntries.sort((a, b) => b.timestamp - a.timestamp); + const toKeep = sortedEntries.slice(0, MAX_ENTRIES_PER_STAT); + // 移除多余条目 + dailyStats = dailyStats.filter(item => item.name !== statName || toKeep.includes(item)); } }); - // 将更新后的数据保存回 localStorage - localStorage.setItem('ld_daily_stats', JSON.stringify(dailyStats)); - - return calculateDailyChanges(dailyStats); - } - - // 计箞24小时内的变化量 - function calculateDailyChanges(dailyStats) { - // 定义要跟踪的数据项 - const statsToTrack = [ - '浏览的话题(所有时间)', // 浏览话题总数 - '回复的话题', // 回复话题数 - '已读帖子(所有时间)', // 已读帖子总数 - '获赞:点赞用户数量', // 获赞数 - '点赞的帖子' // 点赞数 - ]; - - const result = {}; + // 保存更新后的数据 + GM_setValue('ld_daily_stats', dailyStats); - // 对于每个要跟踪的数据项,计算24小时内的变化 + // 计算24小时内每项的变化量 + let changes = {}; statsToTrack.forEach(statName => { - // 过滤出当前数据项的所有记录,并按时间戳排序 - const statRecords = dailyStats - .filter(item => item.name === statName) - .sort((a, b) => a.timestamp - b.timestamp); - - if (statRecords.length >= 2) { - // 获取最早和最新的记录 - const oldest = statRecords[0]; - const newest = statRecords[statRecords.length - 1]; - - // 计算变化量 - const change = newest.value - oldest.value; - - // 存储结果 - result[statName] = change; - } else { - // 如果没有足够的数据点,设置为0 - result[statName] = 0; + const stats = dailyStats.filter(stat => stat.name === statName); + if (stats.length >= 2) { + // 排序数据,最新的在前面 + stats.sort((a, b) => b.timestamp - a.timestamp); + + // 获取最新的值 + const latestValue = stats[0].value; + + // 获取最老的,但不超过24小时的值 + const oldestStats = stats.filter(stat => now - stat.timestamp < 24 * 60 * 60 * 1000); + if (oldestStats.length > 0) { + oldestStats.sort((a, b) => a.timestamp - b.timestamp); + const oldestValue = oldestStats[0].value; + + // 计算变化 + changes[statName] = latestValue - oldestValue; + } } }); - return result; + return changes; } - // 初始加载 - fetchTrustLevelData(); + // 实现闲置检测,避免页面不活跃时进行不必要的刷新 + let refreshInterval; + let visibilityState = true; - // 恢复窗口状态 - // 在所有DOM操作完成后执行,确保 toggleBtn 已经定义 - setTimeout(restorePanelState, 100); + function setupRefreshInterval() { + clearInterval(refreshInterval); + if (visibilityState) { + refreshInterval = setInterval(fetchTrustLevelData, 120000); // 2分钟刷新一次 + } + } - // 定时刷新(每两分钟) - setInterval(fetchTrustLevelData, 120000); + // 监听可见性变化 + document.addEventListener('visibilitychange', () => { + visibilityState = document.visibilityState === 'visible'; + setupRefreshInterval(); + }); + + // 初始化 + function initialize() { + // 恢复面板状态 + restorePanelState(); + + // 首次获取数据 + fetchTrustLevelData(); + + // 设置刷新间隔 + setupRefreshInterval(); + } + + // 页面加载完成后初始化 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initialize); + } else { + initialize(); + } })(); From 3bcb6aa9ba34441e359a7c9488efc7d232664203 Mon Sep 17 00:00:00 2001 From: Michael Kane <15847202+jhhd88@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:37:33 +0800 Subject: [PATCH 2/3] Update LDStatus.user.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 模块化结构:将代码组织为清晰的模块,包括配置、UI、数据处理、事件处理和存储管理,提高了代码的可维护性和可读性。 性能优化: 使用 transform 而非直接修改 left/top 属性进行拖拽,减少重排 合理缓存数据,减少不必要的网络请求 清理过时的历史数据,避免存储溢出 用户体验增强: 添加进度条直观显示完成情况 添加深色/浊色主题切换功能 数据变化高亮显示 改进数据加载和错误处理流程 添加24小时和48小时统计对比,以及趋势指示 可靠性提升: 添加错误处理和离线模式支持 数据请求增加超时处理 自动检查脚本更新功能 代码质量提升: 使用常量配置而非硬编码值 添加详细注释 统一代码风格和命名规范 --- LDStatus.user.js | 1846 ++++++++++++++++++++++++++-------------------- 1 file changed, 1059 insertions(+), 787 deletions(-) diff --git a/LDStatus.user.js b/LDStatus.user.js index a6b11f4..89ab78c 100644 --- a/LDStatus.user.js +++ b/LDStatus.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name LDStatus // @namespace http://tampermonkey.net/ -// @version 1.8 +// @version 2.0 // @description 在 Linux.do 页面显示信任级别进度 // @author 1e0n // @match https://linux.do/* @@ -19,844 +19,1116 @@ (function() { 'use strict'; - // 创建样式 - 使用更特定的选择器以避免影响帖子界面的按钮 - const style = document.createElement('style'); - style.textContent = ` - #ld-trust-level-panel { - position: fixed; - left: 10px; - top: 100px; - width: 210px; - background-color: #2d3748; - border-radius: 8px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); - z-index: 9999; - font-family: Arial, sans-serif; - transition: all 0.3s ease; - overflow: hidden; - color: #e2e8f0; - font-size: 12px; - } + // 模块化结构 + const LDStatus = { + // 配置 + config: { + refreshInterval: 300000, // 5分钟刷新一次 + storageKeys: { + position: 'ld_panel_position', + collapsed: 'ld_panel_collapsed', + theme: 'ld_panel_theme', + lastData: 'ld_last_successful_data' + }, + maxStorageItems: 500, + statsToTrack: [ + '浏览的话题(所有时间)', + '回复的话题', + '已读帖子(所有时间)', + '获赞:点赞用户数量', + '点赞' + ], + nameMapping: { + '已读帖子(所有时间)': '已读帖子(总)', + '浏览的话题(所有时间)': '浏览话题(总)', + '获赞:点赞用户数量': '点赞用户数', + '获赞:单日最高数量': '单日最高获赞', + '被禁言(过去 6 个月)': '被禁言', + '被封禁(过去 6 个月)': '被封禁' + } + }, + + // 变量 + vars: { + panel: null, + header: null, + content: null, + toggleBtn: null, + refreshBtn: null, + updateBtn: null, + themeBtn: null, + isDragging: false, + lastX: 0, + lastY: 0, + refreshTimer: null, + previousRequirements: [], + isDarkTheme: true + }, + + // UI相关方法 + ui: { + // 创建CSS样式 + createStyles: function() { + const style = document.createElement('style'); + style.textContent = ` + /* CSS变量 - 主题颜色 */ + :root { + --ld-bg-color: #ffffff; + --ld-text-color: #1a202c; + --ld-header-bg: #3182ce; + --ld-header-color: #ffffff; + --ld-success-color: #276749; + --ld-fail-color: #c53030; + --ld-border-color: #e2e8f0; + --ld-shadow: 0 0 10px rgba(0, 0, 0, 0.15); + --ld-secondary-color: #4a5568; + --ld-increase-color: #d69e2e; + --ld-decrease-color: #2b6cb0; + --ld-day1-color: #276749; + --ld-day2-color: #2d3748; + } - #ld-trust-level-header { - background-color: #1a202c; - color: white; - padding: 8px 10px; - cursor: move; - display: flex; - justify-content: space-between; - align-items: center; - user-select: none; - } + .ld-dark-theme { + --ld-bg-color: #2d3748; + --ld-text-color: #e2e8f0; + --ld-header-bg: #1a202c; + --ld-header-color: #ffffff; + --ld-success-color: #68d391; + --ld-fail-color: #fc8181; + --ld-border-color: #4a5568; + --ld-shadow: 0 0 10px rgba(0, 0, 0, 0.4); + --ld-secondary-color: #a0aec0; + --ld-increase-color: #ffd700; + --ld-decrease-color: #4299e1; + --ld-day1-color: #68d391; + --ld-day2-color: #cbd5e1; + } - .ld-header-content { - display: flex; - width: 100%; - align-items: center; - justify-content: space-between; - white-space: nowrap; - } + /* 面板基础样式 */ + #ld-trust-level-panel { + position: fixed; + left: 10px; + top: 100px; + width: 210px; + border-radius: 8px; + z-index: 9999; + font-family: Arial, sans-serif; + transition: all 0.3s ease; + overflow: hidden; + font-size: 12px; + background-color: var(--ld-bg-color); + color: var(--ld-text-color); + box-shadow: var(--ld-shadow); + border: 1px solid var(--ld-border-color); + } - .ld-header-content > span:first-child { - margin-right: auto; - font-weight: bold; - } + #ld-trust-level-header { + padding: 8px 10px; + cursor: move; + display: flex; + justify-content: space-between; + align-items: center; + user-select: none; + background-color: var(--ld-header-bg); + color: var(--ld-header-color); + } - #ld-trust-level-content { - padding: 10px; - max-height: none; - overflow-y: visible; - } + .ld-header-content { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + white-space: nowrap; + } - .ld-trust-level-item { - margin-bottom: 6px; - display: flex; - white-space: nowrap; - width: 100%; - justify-content: space-between; - } + .ld-header-content > span:first-child { + margin-right: auto; + font-weight: bold; + } - .ld-trust-level-item .ld-name { - flex: 0 1 auto; - overflow: hidden; - text-overflow: ellipsis; - max-width: 60%; - } + #ld-trust-level-content { + padding: 10px; + max-height: none; + overflow-y: visible; + } - .ld-trust-level-item .ld-value { - font-weight: bold; - flex: 0 0 auto; - text-align: right; - min-width: 70px; - } + .ld-trust-level-item { + margin-bottom: 6px; + display: flex; + white-space: nowrap; + width: 100%; + justify-content: space-between; + } - .ld-trust-level-item.ld-success .ld-value { - color: #68d391; - } + .ld-trust-level-item .ld-name { + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; + max-width: 60%; + } - .ld-trust-level-item.ld-fail .ld-value { - color: #fc8181; - } + .ld-trust-level-item .ld-value { + font-weight: bold; + flex: 0 0 auto; + text-align: right; + min-width: 70px; + } - .ld-toggle-btn, .ld-refresh-btn, .ld-update-btn { - background: none; - border: none; - color: white; - cursor: pointer; - font-size: 14px; - margin-left: 5px; - } + .ld-trust-level-item.ld-success .ld-value { + color: var(--ld-success-color); + } - .ld-version { - font-size: 10px; - color: #a0aec0; - margin-left: 5px; - font-weight: normal; - } + .ld-trust-level-item.ld-fail .ld-value { + color: var(--ld-fail-color); + } - .ld-collapsed { - width: 40px !important; - height: 40px !important; - min-width: 40px !important; - max-width: 40px !important; - border-radius: 8px; - overflow: hidden; - transform: none !important; - } + .ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn { + background: none; + border: none; + color: var(--ld-header-color); + cursor: pointer; + font-size: 14px; + margin-left: 5px; + } - .ld-collapsed #ld-trust-level-header { - justify-content: center; - width: 40px !important; - height: 40px !important; - min-width: 40px !important; - max-width: 40px !important; - padding: 0; - display: flex; - align-items: center; - } + .ld-version { + font-size: 10px; + color: var(--ld-secondary-color); + margin-left: 5px; + font-weight: normal; + } - .ld-collapsed #ld-trust-level-header > div { - justify-content: center; - width: 100%; - height: 100%; - } + .ld-collapsed { + width: 40px !important; + height: 40px !important; + min-width: 40px !important; + max-width: 40px !important; + border-radius: 8px; + overflow: hidden; + transform: none !important; + } - .ld-collapsed #ld-trust-level-content { - display: none !important; - } + .ld-collapsed #ld-trust-level-header { + justify-content: center; + width: 40px !important; + height: 40px !important; + min-width: 40px !important; + max-width: 40px !important; + padding: 0; + display: flex; + align-items: center; + } - .ld-collapsed .ld-header-content > span, - .ld-collapsed .ld-refresh-btn, - .ld-collapsed .ld-update-btn, - .ld-collapsed .ld-version { - display: none !important; - } + .ld-collapsed #ld-trust-level-header > div { + justify-content: center; + width: 100%; + height: 100%; + } - .ld-collapsed .ld-toggle-btn { - margin: 0; - font-size: 16px; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - } + .ld-collapsed #ld-trust-level-content { + display: none !important; + } - .ld-loading { - text-align: center; - padding: 10px; - color: #a0aec0; - } + .ld-collapsed .ld-header-content > span, + .ld-collapsed .ld-refresh-btn, + .ld-collapsed .ld-update-btn, + .ld-collapsed .ld-theme-btn, + .ld-collapsed .ld-version { + display: none !important; + } - .ld-increase { - color: #ffd700; /* 黄色 */ - } + .ld-collapsed .ld-toggle-btn { + margin: 0; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + } - .ld-decrease { - color: #4299e1; /* 蓝色 */ - } + .ld-loading { + text-align: center; + padding: 10px; + color: var(--ld-secondary-color); + } - .ld-daily-stats { - margin-top: 15px; - padding-top: 10px; - border-top: 1px solid #4a5568; - font-size: 11px; - } + .ld-increase { + color: var(--ld-increase-color); + } - .ld-daily-stats-title { - font-weight: bold; - margin-bottom: 5px; - color: #a0aec0; - } + .ld-decrease { + color: var(--ld-decrease-color); + } - .ld-daily-stats-item { - display: flex; - justify-content: space-between; - margin-bottom: 4px; - } + /* 活动数据区域 */ + .ld-daily-stats { + margin-top: 10px; + font-size: 11px; + border-top: 1px solid var(--ld-border-color); + padding-top: 10px; + } - .ld-daily-stats-item .ld-name { - flex: 0 1 auto; - } + .ld-daily-stats-title { + font-weight: bold; + margin-bottom: 5px; + color: var(--ld-secondary-color); + } - .ld-daily-stats-item .ld-value { - flex: 0 0 auto; - font-weight: bold; - color: #68d391; - } - `; - document.head.appendChild(style); - - // 定义存储键 - const STORAGE_KEY_POSITION = 'ld_panel_position'; - const STORAGE_KEY_COLLAPSED = 'ld_panel_collapsed'; - const STORAGE_KEY_LAST_UPDATE_CHECK = 'ld_last_update_check'; - - // 创建面板 - const panel = document.createElement('div'); - panel.id = 'ld-trust-level-panel'; - - // 获取脚本版本号 - const scriptVersion = GM_info.script.version; - - // 创建面板头部 - const header = document.createElement('div'); - header.id = 'ld-trust-level-header'; - - const headerContent = document.createElement('div'); - headerContent.className = 'ld-header-content'; - - const statusSpan = document.createElement('span'); - statusSpan.textContent = 'Status'; - headerContent.appendChild(statusSpan); - - const versionSpan = document.createElement('span'); - versionSpan.className = 'ld-version'; - versionSpan.textContent = `v${scriptVersion}`; - headerContent.appendChild(versionSpan); - - const updateBtn = document.createElement('button'); - updateBtn.className = 'ld-update-btn'; - updateBtn.title = '检查更新'; - updateBtn.textContent = '🔎'; - headerContent.appendChild(updateBtn); - - const refreshBtn = document.createElement('button'); - refreshBtn.className = 'ld-refresh-btn'; - refreshBtn.title = '刷新数据'; - refreshBtn.textContent = '🔄'; - headerContent.appendChild(refreshBtn); - - const toggleBtn = document.createElement('button'); - toggleBtn.className = 'ld-toggle-btn'; - toggleBtn.title = '展开/收起'; - toggleBtn.textContent = '◀'; - headerContent.appendChild(toggleBtn); - - header.appendChild(headerContent); - - // 创建内容区域 - const content = document.createElement('div'); - content.id = 'ld-trust-level-content'; - - // 组装面板 - panel.appendChild(header); - panel.appendChild(content); - document.body.appendChild(panel); - - // 显示加载信息的函数 - function showLoading() { - clearContent(); - const loadingDiv = document.createElement('div'); - loadingDiv.className = 'ld-loading'; - loadingDiv.textContent = '加载中...'; - content.appendChild(loadingDiv); - } - - // 显示错误信息的函数 - function showErrorMessage(message) { - clearContent(); - const errorDiv = document.createElement('div'); - errorDiv.className = 'ld-loading'; - errorDiv.textContent = message; - content.appendChild(errorDiv); - } - - // 清空内容区域的函数 - function clearContent() { - while (content.firstChild) { - content.removeChild(content.firstChild); - } - } - - // 保存窗口位置的函数 - function savePanelPosition() { - const transform = window.getComputedStyle(panel).transform; - if (transform && transform !== 'none') { - const matrix = new DOMMatrix(transform); - GM_setValue(STORAGE_KEY_POSITION, { x: matrix.e, y: matrix.f }); - } - } - - // 保存窗口折叠状态的函数 - function savePanelCollapsedState() { - GM_setValue(STORAGE_KEY_COLLAPSED, panel.classList.contains('ld-collapsed')); - } - - // 恢复窗口状态 - function restorePanelState() { - // 恢复折叠状态 - const isCollapsed = GM_getValue(STORAGE_KEY_COLLAPSED, false); - if (isCollapsed) { - panel.classList.add('ld-collapsed'); - toggleBtn.textContent = '▶'; // 右箭头 - } else { - panel.classList.remove('ld-collapsed'); - toggleBtn.textContent = '◀'; // 左箭头 - } + .ld-daily-stats-item { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + } - // 恢复位置 - const position = GM_getValue(STORAGE_KEY_POSITION, null); - if (position) { - panel.style.transform = `translate(${position.x}px, ${position.y}px)`; - } - } - - // 实现节流函数 - function throttle(func, limit) { - let inThrottle; - return function() { - const args = arguments; - const context = this; - if (!inThrottle) { - func.apply(context, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); + .ld-daily-stats-item .ld-name { + flex: 0 1 auto; + color: inherit; + } + + .ld-daily-stats-item .ld-value { + flex: 0 0 auto; + font-weight: bold; + color: inherit; + } + + /* 两天数据的样式 */ + .ld-dual-stats { + display: flex; + justify-content: flex-end; + gap: 5px; + min-width: 70px; + text-align: right; + } + + .ld-day-stat { + min-width: 25px; + width: 25px; + text-align: right; + display: inline-block; + } + + .ld-day1 { + color: var(--ld-day1-color); + } + + .ld-day2 { + color: var(--ld-day2-color); + } + + .ld-trend-indicator { + margin-left: 2px; + display: inline-block; + min-width: 25px; + width: 25px; + text-align: left; + } + + .ld-stats-header { + display: flex; + justify-content: space-between; + margin-bottom: 6px; + font-size: 10px; + color: inherit; + } + + .ld-stats-header-cols { + display: flex; + gap: 5px; + min-width: 70px; + justify-content: flex-end; + } + + .ld-stats-header-col { + min-width: 25px; + width: 25px; + text-align: center; + } + + .ld-stats-header-trend { + min-width: 25px; + width: 25px; + text-align: center; + } + + /* 进度条样式 */ + .ld-progress-container { + height: 3px; + background-color: var(--ld-border-color); + border-radius: 2px; + margin-top: 3px; + overflow: hidden; + } + + .ld-progress-bar { + height: 100%; + background-color: var(--ld-success-color); + border-radius: 2px; + transition: width 0.3s; + } + + .ld-fail .ld-progress-bar { + background-color: var(--ld-fail-color); + } + + .ld-notice { + font-size: 10px; + color: var(--ld-secondary-color); + text-align: center; + margin-top: 5px; + padding-top: 5px; + border-top: 1px dashed var(--ld-border-color); + } + `; + document.head.appendChild(style); + }, + + // 创建面板 + createPanel: function() { + // 创建主面板 + const panel = document.createElement('div'); + panel.id = 'ld-trust-level-panel'; + LDStatus.vars.panel = panel; + + // 设置默认主题 + const currentTheme = GM_getValue(LDStatus.config.storageKeys.theme, 'dark'); + LDStatus.vars.isDarkTheme = currentTheme === 'dark'; + panel.classList.add(currentTheme === 'dark' ? 'ld-dark-theme' : 'ld-light-theme'); + + // 获取脚本版本号 + const scriptVersion = GM_info.script.version; + + // 创建面板头部 + const header = document.createElement('div'); + header.id = 'ld-trust-level-header'; + header.innerHTML = ` +
+ Status + v${scriptVersion} + + + + +
+ `; + LDStatus.vars.header = header; + + // 创建内容区域 + const content = document.createElement('div'); + content.id = 'ld-trust-level-content'; + content.innerHTML = '
加载中...
'; + LDStatus.vars.content = content; + + // 组装面板 + panel.appendChild(header); + panel.appendChild(content); + document.body.appendChild(panel); + + // 设置按钮引用 + LDStatus.vars.toggleBtn = header.querySelector('.ld-toggle-btn'); + LDStatus.vars.refreshBtn = header.querySelector('.ld-refresh-btn'); + LDStatus.vars.updateBtn = header.querySelector('.ld-update-btn'); + LDStatus.vars.themeBtn = header.querySelector('.ld-theme-btn'); + + // 更新主题按钮图标 + this.updateThemeButtonIcon(); + }, + + // 更新主题按钮图标 + updateThemeButtonIcon: function() { + const themeBtn = LDStatus.vars.themeBtn; + if (!themeBtn) return; + + const isDarkTheme = LDStatus.vars.panel.classList.contains('ld-dark-theme'); + themeBtn.textContent = isDarkTheme ? '🌙' : '☀️'; // 月亮或太阳图标 + themeBtn.title = isDarkTheme ? '切换为亮色主题' : '切换为深色主题'; + }, + + // 显示通知 + showNotice: function(message, type = 'info', duration = 3000) { + const noticeEl = document.createElement('div'); + noticeEl.className = `ld-notice ld-notice-${type}`; + noticeEl.textContent = message; + LDStatus.vars.content.appendChild(noticeEl); + + if (duration > 0) { + setTimeout(() => { + if (noticeEl.parentNode) { + noticeEl.parentNode.removeChild(noticeEl); + } + }, duration); + } + + return noticeEl; + }, + + // 创建进度条 + createProgressBar: function(current, required) { + const currentNum = parseInt(current.match(/\d+/)[0], 10); + const requiredNum = parseInt(required.match(/\d+/)[0], 10); + const percent = Math.min(100, Math.floor((currentNum / requiredNum) * 100)); + + return ` +
+
+
+ `; + }, + + // 渲染信任级别数据 + renderTrustLevelData: function(username, targetLevel, requirements, isMeetingRequirements, dailyChanges = {}) { + const content = LDStatus.vars.content; + + let html = ` +
+ ${username} - 信任级别 ${targetLevel} +
+
+ ${isMeetingRequirements ? '已' : '未'}符合信任级别 ${targetLevel} 要求 +
+ `; + + requirements.forEach(req => { + // 简化项目名称 + let name = req.name; + // 使用配置中的名称映射简化名称 + Object.entries(LDStatus.config.nameMapping).forEach(([original, simplified]) => { + name = name.replace(original, simplified); + }); + + // 提取数字部分以简化显示 + let current = req.current; + let required = req.required; + + // 尝试从字符串中提取数字 + const currentMatch = req.current.match(/(\d+)/); + const requiredMatch = req.required.match(/(\d+)/); + + if (currentMatch) current = currentMatch[1]; + if (requiredMatch) required = requiredMatch[1]; + + // 添加目标完成数变化的标识 + let changeIndicator = ''; + if (req.hasChanged) { + const diff = req.changeValue; + if (diff > 0) { + changeIndicator = ` ▲${diff}`; + } else if (diff < 0) { + changeIndicator = ` ▼${Math.abs(diff)}`; + } + } + + html += ` +
+ ${name} + ${current}${changeIndicator} / ${required} +
+ ${LDStatus.ui.createProgressBar(current, required)} + `; + }); + + // 添加近期活动数据显示 + html += ` +
+
近期的活动
+
+ + + 48h + 24h + + +
+ `; + + // 添加每个数据项 + const dailyStatsItems = [ + { name: '浏览话题', key: '浏览的话题(所有时间)' }, + { name: '回复话题', key: '回复的话题' }, + { name: '已读帖子', key: '已读帖子(所有时间)' }, + { name: '获得点赞', key: '获赞:点赞用户数量' }, + { name: '点赞帖子', key: '点赞' } + ]; + + dailyStatsItems.forEach(item => { + const data = dailyChanges[item.key] || { day1: 0, day2: 0, trend: 0 }; + + // 创建趋势指示器 + let trendIndicator = ''; + if (data.trend > 0) { + trendIndicator = `▲${Math.abs(data.trend)}`; + } else if (data.trend < 0) { + trendIndicator = `▼${Math.abs(data.trend)}`; + } else { + trendIndicator = `0`; + } + + html += ` +
+ ${item.name} + + + ${data.day2} + ${data.day1} + ${trendIndicator} + + +
+ `; + }); + + html += `
`; + + // 检查是否使用缓存数据 + const cachedData = GM_getValue(LDStatus.config.storageKeys.lastData, null); + if (cachedData && !navigator.onLine) { + html += ` +
+ 使用缓存数据,最后更新: ${new Date(cachedData.timestamp).toLocaleString()} +
+ `; + } + + content.innerHTML = html; + }, + + // 渲染缓存数据 + renderCachedData: function() { + const cachedData = GM_getValue(LDStatus.config.storageKeys.lastData, null); + if (cachedData) { + this.renderTrustLevelData( + cachedData.username, + cachedData.targetLevel, + cachedData.requirements, + cachedData.isMeetingRequirements, + cachedData.dailyChanges + ); + this.showNotice(`使用缓存数据,最后更新: ${new Date(cachedData.timestamp).toLocaleString()}`); + } else { + LDStatus.vars.content.innerHTML = '
无可用数据,请检查网络连接
'; + } } - }; - } - - // 拖动功能 - let isDragging = false; - let lastX, lastY; - - header.addEventListener('mousedown', (e) => { - if (panel.classList.contains('ld-collapsed')) return; - - isDragging = true; - lastX = e.clientX; - lastY = e.clientY; - - // 添加拖动时的样式 - panel.style.transition = 'none'; - document.body.style.userSelect = 'none'; - }); - - document.addEventListener('mousemove', throttle((e) => { - if (!isDragging) return; - - const dx = e.clientX - lastX; - const dy = e.clientY - lastY; - lastX = e.clientX; - lastY = e.clientY; - - requestAnimationFrame(() => { - const currentTransform = window.getComputedStyle(panel).transform; - const matrix = new DOMMatrix(currentTransform === 'none' ? '' : currentTransform); - const newX = matrix.e + dx; - const newY = matrix.f + dy; - panel.style.transform = `translate(${newX}px, ${newY}px)`; - }); - }, 16)); - - document.addEventListener('mouseup', () => { - if (!isDragging) return; - - isDragging = false; - panel.style.transition = ''; - document.body.style.userSelect = ''; - - // 保存窗口位置 - savePanelPosition(); - }); - - // 展开/收起功能 - toggleBtn.addEventListener('click', () => { - panel.classList.toggle('ld-collapsed'); - toggleBtn.textContent = panel.classList.contains('ld-collapsed') ? '▶' : '◀'; - - // 保存折叠状态 - savePanelCollapsedState(); - }); - - // 刷新按钮 - refreshBtn.addEventListener('click', fetchTrustLevelData); - - // 检查更新按钮 - updateBtn.addEventListener('click', checkForUpdates); - - // 检查脚本更新 - function checkForUpdates() { - const lastCheck = GM_getValue(STORAGE_KEY_LAST_UPDATE_CHECK, 0); - const now = Date.now(); - - // 一天只检查一次 - if (now - lastCheck < 86400000) { - updateBtn.textContent = '⏱️'; - updateBtn.title = '今天已检查过更新'; - setTimeout(() => { - updateBtn.textContent = '🔎'; - updateBtn.title = '检查更新'; - }, 2000); - return; - } - - const updateURL = 'https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js'; - - // 显示正在检查的状态 - updateBtn.textContent = '⌛'; // 沙漏图标 - updateBtn.title = '正在检查更新...'; - - GM_xmlhttpRequest({ - method: 'GET', - url: updateURL, - timeout: 10000, // 添加超时设置 - onload: function(response) { - if (response.status === 200) { - try { - // 提取远程脚本的版本号 - const versionMatch = response.responseText.match(/@version\s+([\d\.]+)/); - if (versionMatch && versionMatch[1]) { - const remoteVersion = versionMatch[1]; - - // 比较版本 - if (remoteVersion > scriptVersion) { - // 有新版本 - updateBtn.textContent = '⚠️'; // 警告图标 - updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`; - updateBtn.style.color = '#ffd700'; // 黄色 - - // 点击按钮跳转到更新页面 - updateBtn.onclick = function() { - window.open(updateURL, '_blank'); - }; + }, + + // 数据处理相关方法 + data: { + // 获取信任级别数据 + fetchTrustLevelData: function() { + LDStatus.vars.content.innerHTML = '
加载中...
'; + + // 如果离线,使用缓存数据 + if (!navigator.onLine) { + LDStatus.ui.renderCachedData(); + return; + } + + GM_xmlhttpRequest({ + method: 'GET', + url: 'https://connect.linux.do', + timeout: 10000, // 设置超时时间 + onload: function(response) { + try { + if (response.status === 200) { + LDStatus.data.parseTrustLevelData(response.responseText); } else { - // 已是最新版本 - updateBtn.textContent = '✔'; // 勾选图标 - updateBtn.title = '已是最新版本'; - updateBtn.style.color = '#68d391'; // 绿色 - - // 3秒后恢复原样式 - setTimeout(() => { - updateBtn.textContent = '🔎'; // 放大镜图标 - updateBtn.title = '检查更新'; - updateBtn.style.color = 'white'; - updateBtn.onclick = checkForUpdates; - }, 3000); + throw new Error(`HTTP错误: ${response.status}`); } - } else { - handleUpdateError('无法解析版本信息'); + } catch (error) { + console.error('数据处理错误:', error); + LDStatus.vars.content.innerHTML = `
处理数据时出错: ${error.message}
`; + // 尝试使用缓存数据 + LDStatus.ui.renderCachedData(); } - } catch (error) { - handleUpdateError('处理更新信息时出错: ' + error.message); + }, + onerror: function(error) { + console.error('请求错误:', error); + LDStatus.vars.content.innerHTML = '
网络请求失败,请检查网络连接
'; + // 尝试使用缓存数据 + LDStatus.ui.renderCachedData(); + }, + ontimeout: function() { + LDStatus.vars.content.innerHTML = '
请求超时,请稍后再试
'; + // 尝试使用缓存数据 + LDStatus.ui.renderCachedData(); } - } else { - handleUpdateError(`请求失败 (${response.status})`); - } - - // 更新检查时间 - GM_setValue(STORAGE_KEY_LAST_UPDATE_CHECK, now); - }, - onerror: function(error) { - handleUpdateError('网络请求失败'); + }); }, - ontimeout: function() { - handleUpdateError('请求超时'); - } - }); - - // 处理更新检查错误 - function handleUpdateError(message) { - console.error('检查更新失败:', message); - updateBtn.textContent = '❌'; // 错误图标 - updateBtn.title = '检查更新失败,请稍后再试'; - updateBtn.style.color = '#fc8181'; // 红色 - - // 3秒后恢复原样式 - setTimeout(() => { - updateBtn.textContent = '🔎'; // 放大镜图标 - updateBtn.title = '检查更新'; - updateBtn.style.color = 'white'; - }, 3000); - } - } - - // 获取信任级别数据 - function fetchTrustLevelData() { - showLoading(); - - GM_xmlhttpRequest({ - method: 'GET', - url: 'https://connect.linux.do', - timeout: 10000, // 添加超时设置 - onload: function(response) { - if (response.status === 200) { - try { - parseTrustLevelData(response.responseText); - } catch (error) { - console.error('解析数据时出错:', error); - showErrorMessage('解析数据时出错: ' + error.message); + + // 解析信任级别数据 + parseTrustLevelData: function(html) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + // 查找信任级别区块 + const trustLevelSection = Array.from(doc.querySelectorAll('.bg-white.p-6.rounded-lg')).find(div => { + const heading = div.querySelector('h2'); + return heading && heading.textContent.includes('信任级别'); + }); + + if (!trustLevelSection) { + LDStatus.vars.content.innerHTML = '
未找到信任级别数据,请确保已登录
'; + return; + } + + // 获取用户名和当前级别 + const heading = trustLevelSection.querySelector('h2').textContent.trim(); + const match = heading.match(/(.*) - 信任级别 (\d+) 的要求/); + const username = match ? match[1] : '未知用户'; + const targetLevel = match ? match[2] : '未知'; + + // 获取表格数据 + const tableRows = trustLevelSection.querySelectorAll('table tr'); + const requirements = []; + + for (let i = 1; i < tableRows.length; i++) { // 跳过表头 + const row = tableRows[i]; + const cells = row.querySelectorAll('td'); + + if (cells.length >= 3) { + const name = cells[0].textContent.trim(); + const current = cells[1].textContent.trim(); + const required = cells[2].textContent.trim(); + const isSuccess = cells[1].classList.contains('text-green-500'); + + // 提取当前完成数的数字部分 + const currentMatch = current.match(/(\d+)/); + const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0; + // 查找上一次的数据记录 + let changeValue = 0; + let hasChanged = false; + + if (LDStatus.vars.previousRequirements.length > 0) { + const prevReq = LDStatus.vars.previousRequirements.find(pr => pr.name === name); + if (prevReq) { + // 如果完成数有变化,更新变化值 + if (currentValue !== prevReq.currentValue) { + changeValue = currentValue - prevReq.currentValue; + hasChanged = true; + } else if (prevReq.changeValue) { + // 如果完成数没有变化,但之前有变化值,保留之前的变化值 + changeValue = prevReq.changeValue; + hasChanged = true; + } + } + } + + requirements.push({ + name, + current, + required, + isSuccess, + currentValue, + changeValue, // 变化值 + hasChanged // 是否有变化 + }); } - } else { - showErrorMessage(`获取数据失败 (${response.status})`); } + + // 获取总体结果 + const resultText = trustLevelSection.querySelector('p.text-red-500, p.text-green-500'); + const isMeetingRequirements = resultText ? !resultText.classList.contains('text-red-500') : false; + + // 存储48小时内的数据变化 + const dailyChanges = this.saveDailyStats(requirements); + + // 缓存数据,以备网络问题时使用 + GM_setValue(LDStatus.config.storageKeys.lastData, { + username, + targetLevel, + requirements, + isMeetingRequirements, + dailyChanges, + timestamp: new Date().getTime() + }); + + // 渲染数据 + LDStatus.ui.renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges); + + // 保存当前数据作为下次比较的基准 + LDStatus.vars.previousRequirements = [...requirements]; }, - onerror: function(error) { - console.error('请求错误:', error); - showErrorMessage('网络请求失败'); + + // 存储48小时内的数据变化 + saveDailyStats: function(requirements) { + const statsToTrack = LDStatus.config.statsToTrack; + + // 获取当前时间 + const now = new Date().getTime(); + + // 从 localStorage 中获取已存储的数据 + let dailyStats = JSON.parse(localStorage.getItem('ld_daily_stats') || '[]'); + + // 删除超过48小时的数据 + const twoDaysAgo = now - 48 * 60 * 60 * 1000; + dailyStats = dailyStats.filter(item => item.timestamp > twoDaysAgo); + + // 对于每个要跟踪的数据项,找到当前值并添加到历史记录中 + statsToTrack.forEach(statName => { + const req = requirements.find(r => r.name === statName); + if (req) { + // 提取数字值 + const currentMatch = req.current.match(/(\d+)/); + const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0; + + // 添加新的数据点 + dailyStats.push({ + name: statName, + value: currentValue, + timestamp: now + }); + } + }); + + // 清理过量的历史数据 + this.cleanupStorage(dailyStats); + + // 将更新后的数据保存回 localStorage + localStorage.setItem('ld_daily_stats', JSON.stringify(dailyStats)); + + return this.calculateDailyChanges(dailyStats); }, - ontimeout: function() { - showErrorMessage('请求超时'); - } - }); - } - - // 查找信任级别区块 - function findTrustLevelSection(doc) { - const headers = doc.querySelectorAll('h2'); - const trustHeader = Array.from(headers).find(h => h.textContent.includes('信任级别')); - return trustHeader ? trustHeader.closest('.bg-white.p-6.rounded-lg') : null; - } - - // 提取用户信息 - function extractUserInfo(section) { - const heading = section.querySelector('h2').textContent.trim(); - const match = heading.match(/(.*) - 信任级别 (\d+) 的要求/); - return { - username: match ? match[1] : '未知用户', - targetLevel: match ? match[2] : '未知' - }; - } - - // 检查需求状态 - function checkRequirementStatus(section) { - const resultText = section.querySelector('p.text-red-500, p.text-green-500'); - return resultText ? !resultText.classList.contains('text-red-500') : false; - } - - // 提取需求数据 - function extractRequirements(section, previousRequirements) { - const tableRows = section.querySelectorAll('table tr'); - const requirements = []; - - for (let i = 1; i < tableRows.length; i++) { // 跳过表头 - const row = tableRows[i]; - const cells = row.querySelectorAll('td'); - - if (cells.length >= 3) { - const name = cells[0].textContent.trim(); - const current = cells[1].textContent.trim(); - const required = cells[2].textContent.trim(); - const isSuccess = cells[1].classList.contains('text-green-500'); - - // 提取当前完成数的数字部分 - const currentMatch = current.match(/(\d+)/); - const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0; - - // 查找上一次的数据记录 - let changeValue = 0; - let hasChanged = false; - - if (previousRequirements.length > 0) { - const prevReq = previousRequirements.find(pr => pr.name === name); - if (prevReq) { - // 如果完成数有变化,更新变化值 - if (currentValue !== prevReq.currentValue) { - changeValue = currentValue - prevReq.currentValue; - hasChanged = true; - } else if (prevReq.changeValue) { - // 如果完成数没有变化,但之前有变化值,保留之前的变化值 - changeValue = prevReq.changeValue; - hasChanged = true; + + // 清理过量的存储数据 + cleanupStorage: function(stats) { + const maxItems = LDStatus.config.maxStorageItems; + + if (stats.length > maxItems) { + // 按时间戳排序并只保留最新的数据 + stats.sort((a, b) => b.timestamp - a.timestamp); + return stats.slice(0, maxItems); + } + + return stats; + }, + + // 计算近两天内的变化量 + calculateDailyChanges: function(dailyStats) { + const statsToTrack = LDStatus.config.statsToTrack; + const result = {}; + const now = new Date().getTime(); + const oneDayAgo = now - 24 * 60 * 60 * 1000; + const twoDaysAgo = now - 48 * 60 * 60 * 1000; + + // 对于每个要跟踪的数据项,计算两天内的变化 + statsToTrack.forEach(statName => { + // 过滤出当前数据项的所有记录,并按时间戳排序 + const statRecords = dailyStats + .filter(item => item.name === statName) + .sort((a, b) => a.timestamp - b.timestamp); + + // 初始化结果对象结构 + result[statName] = { + day1: 0, // 最近24小时 + day2: 0, // 24-48小时 + trend: 0 // 趋势:day1 - day2 + }; + + if (statRecords.length >= 2) { + // 找出最新记录和其前面两个时间段的记录 + const newest = statRecords[statRecords.length - 1]; + + // 找最近24小时内最早的记录 + const oldestDay1 = statRecords.filter(item => item.timestamp > oneDayAgo)[0]; + + // 找24-48小时内最早的记录和最新的记录 + const recordsDay2 = statRecords.filter(item => + item.timestamp <= oneDayAgo && item.timestamp > twoDaysAgo); + + const oldestDay2 = recordsDay2.length > 0 ? recordsDay2[0] : null; + const newestDay2 = recordsDay2.length > 0 ? + recordsDay2[recordsDay2.length - 1] : null; + + // 计算最近24小时的变化 + if (oldestDay1) { + result[statName].day1 = newest.value - oldestDay1.value; + } + + // 计算24-48小时的变化 + if (oldestDay2 && newestDay2) { + result[statName].day2 = newestDay2.value - oldestDay2.value; } + + // 计算趋势(今天和昨天的变化差异) + result[statName].trend = result[statName].day1 - result[statName].day2; } - } + }); - requirements.push({ - name, - current, - required, - isSuccess, - currentValue, - changeValue, // 变化值 - hasChanged // 是否有变化 + return result; + }, + + // 检查脚本更新 + checkForUpdates: function() { + const updateURL = 'https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js'; + const updateBtn = LDStatus.vars.updateBtn; + + // 显示正在检查的状态 + updateBtn.textContent = '⌛'; // 沙漏图标 + updateBtn.title = '正在检查更新...'; + + GM_xmlhttpRequest({ + method: 'GET', + url: updateURL, + onload: function(response) { + if (response.status === 200) { + // 提取远程脚本的版本号 + const versionMatch = response.responseText.match(/@version\s+([\d\.]+)/); + if (versionMatch && versionMatch[1]) { + const remoteVersion = versionMatch[1]; + const scriptVersion = GM_info.script.version; + + // 比较版本 + if (remoteVersion > scriptVersion) { + // 有新版本 + updateBtn.textContent = '⚠️'; // 警告图标 + updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`; + updateBtn.style.color = 'var(--ld-increase-color)'; // 黄色 + + // 点击按钮跳转到更新页面 + updateBtn.onclick = function() { + window.open(updateURL, '_blank'); + }; + } else { + // 已是最新版本 + updateBtn.textContent = '✔'; // 勾选图标 + updateBtn.title = '已是最新版本'; + updateBtn.style.color = 'var(--ld-success-color)'; // 绿色 + + // 3秒后恢复原样式 + setTimeout(() => { + updateBtn.textContent = '🔎'; // 放大镜图标 + updateBtn.title = '检查更新'; + updateBtn.style.color = ''; + updateBtn.onclick = LDStatus.events.onUpdateBtnClick; + }, 3000); + } + } else { + LDStatus.data.handleUpdateError(); + } + } else { + LDStatus.data.handleUpdateError(); + } + }, + onerror: LDStatus.data.handleUpdateError }); + }, + + // 处理更新检查错误 + handleUpdateError: function() { + const updateBtn = LDStatus.vars.updateBtn; + updateBtn.textContent = '❌'; // 错误图标 + updateBtn.title = '检查更新失败,请稍后再试'; + updateBtn.style.color = 'var(--ld-fail-color)'; // 红色 + + // 3秒后恢复原样式 + setTimeout(() => { + updateBtn.textContent = '🔎'; // 放大镜图标 + updateBtn.title = '检查更新'; + updateBtn.style.color = ''; + }, 3000); } - } + }, + + // 事件处理相关方法 + events: { + // 注册所有事件监听 + registerEvents: function() { + // 拖动面板相关事件 + this.setupDragEvents(); + + // 按钮点击事件 + LDStatus.vars.toggleBtn.addEventListener('click', this.onToggleBtnClick); + LDStatus.vars.refreshBtn.addEventListener('click', this.onRefreshBtnClick); + LDStatus.vars.updateBtn.addEventListener('click', this.onUpdateBtnClick); + LDStatus.vars.themeBtn.addEventListener('click', this.onThemeBtnClick); + + // 页面可见性变化时刷新数据 + document.addEventListener('visibilitychange', this.onVisibilityChange); + }, - return requirements; - } + // 设置拖动面板的事件 + setupDragEvents: function() { + const header = LDStatus.vars.header; - // 解析信任级别数据 - function parseTrustLevelData(html) { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); + header.addEventListener('mousedown', this.onPanelDragStart); + document.addEventListener('mousemove', this.onPanelDragMove); + document.addEventListener('mouseup', this.onPanelDragEnd); + }, - // 查找信任级别区块 - const trustLevelSection = findTrustLevelSection(doc); + // 面板拖动开始 + onPanelDragStart: function(e) { + if (LDStatus.vars.panel.classList.contains('ld-collapsed')) return; - if (!trustLevelSection) { - showErrorMessage('未找到信任级别数据,请确保已登录'); - return; - } + LDStatus.vars.isDragging = true; + LDStatus.vars.lastX = e.clientX; + LDStatus.vars.lastY = e.clientY; + + // 添加拖动时的样式 + LDStatus.vars.panel.style.transition = 'none'; + document.body.style.userSelect = 'none'; + }, + + // 面板拖动中 + onPanelDragMove: function(e) { + if (!LDStatus.vars.isDragging) return; + + // 使用 transform 而不是改变 left/top 属性,性能更好 + const dx = e.clientX - LDStatus.vars.lastX; + const dy = e.clientY - LDStatus.vars.lastY; + + const currentTransform = window.getComputedStyle(LDStatus.vars.panel).transform; + const matrix = new DOMMatrix(currentTransform === 'none' ? '' : currentTransform); + + const newX = matrix.e + dx; + const newY = matrix.f + dy; + + LDStatus.vars.panel.style.transform = `translate(${newX}px, ${newY}px)`; + + LDStatus.vars.lastX = e.clientX; + LDStatus.vars.lastY = e.clientY; + }, + + // 面板拖动结束 + onPanelDragEnd: function() { + if (!LDStatus.vars.isDragging) return; + + LDStatus.vars.isDragging = false; + LDStatus.vars.panel.style.transition = ''; + document.body.style.userSelect = ''; + + // 保存窗口位置 + LDStatus.storage.savePanelPosition(); + }, + + // 折叠/展开面板按钮点击 + onToggleBtnClick: function() { + const panel = LDStatus.vars.panel; + panel.classList.toggle('ld-collapsed'); + LDStatus.vars.toggleBtn.textContent = panel.classList.contains('ld-collapsed') ? '▶' : '◀'; + + // 保存折叠状态 + LDStatus.storage.savePanelCollapsedState(); + }, + + // 刷新按钮点击 + onRefreshBtnClick: function() { + LDStatus.data.fetchTrustLevelData(); + }, + + // 更新按钮点击 + onUpdateBtnClick: function() { + LDStatus.data.checkForUpdates(); + }, + + // 主题按钮点击 + onThemeBtnClick: function() { + const panel = LDStatus.vars.panel; + const isDarkTheme = panel.classList.contains('ld-dark-theme'); + + // 切换主题类 + panel.classList.remove(isDarkTheme ? 'ld-dark-theme' : 'ld-light-theme'); + panel.classList.add(isDarkTheme ? 'ld-light-theme' : 'ld-dark-theme'); - // 获取用户名和当前级别 - const { username, targetLevel } = extractUserInfo(trustLevelSection); - - // 获取表格数据 - const requirements = extractRequirements(trustLevelSection, previousRequirements); - - // 获取总体结果 - const isMeetingRequirements = checkRequirementStatus(trustLevelSection); - - // 存储24小时内的数据变化 - const dailyChanges = saveDailyStats(requirements); - - // 渲染数据 - renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges); - - // 保存当前数据作为下次比较的基准 - previousRequirements = [...requirements]; - } - - // 渲染信任级别数据 - function renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges = {}) { - clearContent(); - - const fragment = document.createDocumentFragment(); - - // 创建用户和级别信息 - const headerDiv = document.createElement('div'); - headerDiv.style.marginBottom = '8px'; - headerDiv.style.fontWeight = 'bold'; - headerDiv.textContent = `${username} - 信任级别 ${targetLevel}`; - fragment.appendChild(headerDiv); - - // 创建需求状态信息 - const statusDiv = document.createElement('div'); - statusDiv.style.marginBottom = '10px'; - statusDiv.style.color = isMeetingRequirements ? '#68d391' : '#fc8181'; - statusDiv.style.fontSize = '11px'; - statusDiv.textContent = `${isMeetingRequirements ? '已' : '未'}符合信任级别 ${targetLevel} 要求`; - fragment.appendChild(statusDiv); - - // 创建需求列表 - requirements.forEach(req => { - // 简化项目名称 - let name = req.name; - // 将一些常见的长名称缩短 - name = name.replace('已读帖子(所有时间)', '已读帖子(总)'); - name = name.replace('浏览的话题(所有时间)', '浏览话题(总)'); - name = name.replace('获赞:点赞用户数量', '点赞用户数'); - name = name.replace('获赞:单日最高数量', '单日最高获赞'); - name = name.replace('被禁言(过去 6 个月)', '被禁言'); - name = name.replace('被封禁(过去 6 个月)', '被封禁'); - - // 提取数字部分以简化显示 - let current = req.current; - let required = req.required; - - // 尝试从字符串中提取数字 - const currentMatch = req.current.match(/(\d+)/); - const requiredMatch = req.required.match(/(\d+)/); - - if (currentMatch) current = currentMatch[1]; - if (requiredMatch) required = requiredMatch[1]; - - // 创建需求项 - const reqDiv = document.createElement('div'); - reqDiv.className = `ld-trust-level-item ${req.isSuccess ? 'ld-success' : 'ld-fail'}`; - - const nameSpan = document.createElement('span'); - nameSpan.className = 'ld-name'; - nameSpan.textContent = name; - reqDiv.appendChild(nameSpan); - - const valueSpan = document.createElement('span'); - valueSpan.className = 'ld-value'; - - // 添加目标完成数 - valueSpan.textContent = `${current} / ${required}`; - - // 添加变化指示器 - if (req.hasChanged) { - const changeIndicator = document.createElement('span'); - const diff = req.changeValue; - if (diff > 0) { - changeIndicator.className = 'ld-increase'; - changeIndicator.textContent = ` ▲${diff}`; - } else if (diff < 0) { - changeIndicator.className = 'ld-decrease'; - changeIndicator.textContent = ` ▼${Math.abs(diff)}`; + // 更新主题变量 + LDStatus.vars.isDarkTheme = !isDarkTheme; + + // 更新按钮图标 + LDStatus.ui.updateThemeButtonIcon(); + + // 保存主题设置 + GM_setValue(LDStatus.config.storageKeys.theme, LDStatus.vars.isDarkTheme ? 'dark' : 'light'); + }, + + // 页面可见性变化处理 + onVisibilityChange: function() { + if (!document.hidden) { + // 检查上次刷新时间,如果超过指定间隔则刷新数据 + const lastRefreshTime = LDStatus.vars.lastRefreshTime || 0; + const now = Date.now(); + + if (now - lastRefreshTime > LDStatus.config.refreshInterval) { + LDStatus.data.fetchTrustLevelData(); + LDStatus.vars.lastRefreshTime = now; + } } - valueSpan.appendChild(changeIndicator); - } - - reqDiv.appendChild(valueSpan); - fragment.appendChild(reqDiv); - }); - - // 创建24小时活动数据 - const dailyStatsDiv = document.createElement('div'); - dailyStatsDiv.className = 'ld-daily-stats'; - - const dailyStatsTitleDiv = document.createElement('div'); - dailyStatsTitleDiv.className = 'ld-daily-stats-title'; - dailyStatsTitleDiv.textContent = '24小时内的活动'; - dailyStatsDiv.appendChild(dailyStatsTitleDiv); - - // 添加每个数据项 - const dailyStatsItems = [ - { name: '浏览话题', key: '浏览的话题(所有时间)' }, - { name: '回复话题', key: '回复的话题' }, - { name: '已读帖子', key: '已读帖子(所有时间)' }, - { name: '获得点赞', key: '获赞:点赞用户数量' }, - { name: '点赞帖子', key: '点赞的帖子' } - ]; - - dailyStatsItems.forEach(item => { - const value = dailyChanges[item.key] || 0; - - const statsItemDiv = document.createElement('div'); - statsItemDiv.className = 'ld-daily-stats-item'; - - const nameSpan = document.createElement('span'); - nameSpan.className = 'ld-name'; - nameSpan.textContent = item.name; - statsItemDiv.appendChild(nameSpan); - - const valueSpan = document.createElement('span'); - valueSpan.className = 'ld-value'; - valueSpan.textContent = value; - statsItemDiv.appendChild(valueSpan); - - dailyStatsDiv.appendChild(statsItemDiv); - }); - - fragment.appendChild(dailyStatsDiv); - content.appendChild(fragment); - } - - // 存储上一次获取的数据,用于比较变化 - let previousRequirements = []; - - // 存储24小时内的数据变化 - function saveDailyStats(requirements) { - // 定义要跟踪的数据项 - const statsToTrack = [ - '浏览的话题(所有时间)', // 浏览话题总数 - '回复的话题', // 回复话题数 - '已读帖子(所有时间)', // 已读帖子总数 - '获赞:点赞用户数量', // 获得点赞 - '点赞的帖子' // 点赞帖子 - ]; - - // 从存储中获取之前的记录 - let dailyStats = GM_getValue('ld_daily_stats', []); - - // 获取当前时间戳 - const now = Date.now(); - - // 清理超过24小时的旧数据 - dailyStats = dailyStats.filter(stat => now - stat.timestamp < 24 * 60 * 60 * 1000); - - // 提取要跟踪的数据项 - const trackedStats = requirements.filter(req => statsToTrack.includes(req.name)); - - // 为每个要跟踪的项目添加新记录 - trackedStats.forEach(stat => { - dailyStats.push({ - name: stat.name, - value: stat.currentValue, - timestamp: now - }); - }); - - // 限制每种统计类型的条目数,防止过度存储 - const MAX_ENTRIES_PER_STAT = 50; - statsToTrack.forEach(statName => { - const statEntries = dailyStats.filter(item => item.name === statName); - if (statEntries.length > MAX_ENTRIES_PER_STAT) { - // 只保留最新的 MAX_ENTRIES_PER_STAT 条记录 - const sortedEntries = statEntries.sort((a, b) => b.timestamp - a.timestamp); - const toKeep = sortedEntries.slice(0, MAX_ENTRIES_PER_STAT); - // 移除多余条目 - dailyStats = dailyStats.filter(item => item.name !== statName || toKeep.includes(item)); } - }); - - // 保存更新后的数据 - GM_setValue('ld_daily_stats', dailyStats); - - // 计算24小时内每项的变化量 - let changes = {}; - statsToTrack.forEach(statName => { - const stats = dailyStats.filter(stat => stat.name === statName); - if (stats.length >= 2) { - // 排序数据,最新的在前面 - stats.sort((a, b) => b.timestamp - a.timestamp); - - // 获取最新的值 - const latestValue = stats[0].value; - - // 获取最老的,但不超过24小时的值 - const oldestStats = stats.filter(stat => now - stat.timestamp < 24 * 60 * 60 * 1000); - if (oldestStats.length > 0) { - oldestStats.sort((a, b) => a.timestamp - b.timestamp); - const oldestValue = oldestStats[0].value; - - // 计算变化 - changes[statName] = latestValue - oldestValue; + }, + + // 存储相关方法 + storage: { + // 初始化存储 + initStorage: function() { + // 恢复面板位置 + this.restorePanelPosition(); + + // 恢复折叠状态 + this.restorePanelCollapsedState(); + }, + + // 保存面板位置 + savePanelPosition: function() { + const style = window.getComputedStyle(LDStatus.vars.panel); + const transform = style.transform; + + if (transform !== 'none') { + GM_setValue(LDStatus.config.storageKeys.position, transform); + } + }, + + // 恢复面板位置 + restorePanelPosition: function() { + const savedPosition = GM_getValue(LDStatus.config.storageKeys.position, null); + + if (savedPosition) { + LDStatus.vars.panel.style.transform = savedPosition; + } + }, + + // 保存面板折叠状态 + savePanelCollapsedState: function() { + const isCollapsed = LDStatus.vars.panel.classList.contains('ld-collapsed'); + GM_setValue(LDStatus.config.storageKeys.collapsed, isCollapsed); + }, + + // 恢复面板折叠状态 + restorePanelCollapsedState: function() { + const isCollapsed = GM_getValue(LDStatus.config.storageKeys.collapsed, false); + + if (isCollapsed) { + LDStatus.vars.panel.classList.add('ld-collapsed'); + LDStatus.vars.toggleBtn.textContent = '▶'; + } else { + LDStatus.vars.panel.classList.remove('ld-collapsed'); + LDStatus.vars.toggleBtn.textContent = '◀'; } } - }); + }, + + // 初始化方法 + init: function() { + // 初始化 UI + this.ui.createStyles(); + this.ui.createPanel(); - return changes; - } + // 初始化存储 + this.storage.initStorage(); - // 实现闲置检测,避免页面不活跃时进行不必要的刷新 - let refreshInterval; - let visibilityState = true; + // 注册事件 + this.events.registerEvents(); - function setupRefreshInterval() { - clearInterval(refreshInterval); - if (visibilityState) { - refreshInterval = setInterval(fetchTrustLevelData, 120000); // 2分钟刷新一次 + // 获取数据 + this.data.fetchTrustLevelData(); + + // 设置定时刷新 + LDStatus.vars.refreshTimer = setInterval(function() { + LDStatus.data.fetchTrustLevelData(); + LDStatus.vars.lastRefreshTime = Date.now(); + }, this.config.refreshInterval); + + // 记录初始化时间 + LDStatus.vars.lastRefreshTime = Date.now(); } - } - - // 监听可见性变化 - document.addEventListener('visibilitychange', () => { - visibilityState = document.visibilityState === 'visible'; - setupRefreshInterval(); - }); - - // 初始化 - function initialize() { - // 恢复面板状态 - restorePanelState(); - - // 首次获取数据 - fetchTrustLevelData(); - - // 设置刷新间隔 - setupRefreshInterval(); - } - - // 页面加载完成后初始化 - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initialize); - } else { - initialize(); - } + }; + + // 初始化脚本 + LDStatus.init(); })(); From 885b562cc297ba714b41ad21cbf925c769156e4f Mon Sep 17 00:00:00 2001 From: Michael Kane <15847202+jhhd88@users.noreply.github.com> Date: Wed, 21 May 2025 08:46:32 +0800 Subject: [PATCH 3/3] Update LDStatus.user.js --- LDStatus.user.js | 376 +++++++++++++++++++++++++++++++---------------- 1 file changed, 249 insertions(+), 127 deletions(-) diff --git a/LDStatus.user.js b/LDStatus.user.js index 89ab78c..cacb656 100644 --- a/LDStatus.user.js +++ b/LDStatus.user.js @@ -45,6 +45,23 @@ '获赞:单日最高数量': '单日最高获赞', '被禁言(过去 6 个月)': '被禁言', '被封禁(过去 6 个月)': '被封禁' + }, + icons: { + SEARCH: '🔎', + REFRESH: '🔄', + THEME_DARK: '🌙', + THEME_LIGHT: '☀️', + COLLAPSE: '◀', + EXPAND: '▶', + LOADING: '⌛', + UPDATE_AVAILABLE: '⚠️', + UP_TO_DATE: '✔', + ERROR: '❌', + ARROW_UP: '▲', + ARROW_DOWN: '▼', + TREND_INCREASE: '▲', + TREND_DECREASE: '▼', + TREND_STABLE: '–' // Using '–' for better visual than '0' } }, @@ -405,10 +422,10 @@
Status v${scriptVersion} - - - - + + + +
`; LDStatus.vars.header = header; @@ -440,7 +457,7 @@ if (!themeBtn) return; const isDarkTheme = LDStatus.vars.panel.classList.contains('ld-dark-theme'); - themeBtn.textContent = isDarkTheme ? '🌙' : '☀️'; // 月亮或太阳图标 + themeBtn.textContent = isDarkTheme ? LDStatus.config.icons.THEME_DARK : LDStatus.config.icons.THEME_LIGHT; themeBtn.title = isDarkTheme ? '切换为亮色主题' : '切换为深色主题'; }, @@ -464,9 +481,21 @@ // 创建进度条 createProgressBar: function(current, required) { - const currentNum = parseInt(current.match(/\d+/)[0], 10); - const requiredNum = parseInt(required.match(/\d+/)[0], 10); - const percent = Math.min(100, Math.floor((currentNum / requiredNum) * 100)); + // Ensure current and required are strings before trying to match + const currentStr = String(current || ''); // Default to empty string if null/undefined + const requiredStr = String(required || ''); // Default to empty string + + const currentNumMatch = currentStr.match(/\d+/); + const requiredNumMatch = requiredStr.match(/\d+/); + + // Default to 0 if parsing fails to avoid NaN errors or errors from null[0] + const currentNum = (currentNumMatch && currentNumMatch[0]) ? parseInt(currentNumMatch[0], 10) : 0; + const requiredNum = (requiredNumMatch && requiredNumMatch[0]) ? parseInt(requiredNumMatch[0], 10) : 0; + + // Avoid division by zero if requiredNum is 0 + const percent = (requiredNum > 0) + ? Math.min(100, Math.floor((currentNum / requiredNum) * 100)) + : (currentNum > 0 ? 100 : 0); // If required is 0, 100% if current > 0, else 0% return `
@@ -496,34 +525,29 @@ name = name.replace(original, simplified); }); - // 提取数字部分以简化显示 - let current = req.current; - let required = req.required; - - // 尝试从字符串中提取数字 - const currentMatch = req.current.match(/(\d+)/); - const requiredMatch = req.required.match(/(\d+)/); - - if (currentMatch) current = currentMatch[1]; - if (requiredMatch) required = requiredMatch[1]; + // For display, use the text directly from req.current and req.required (which are already strings) + // The numerical parsing for logic (currentValue) is done in parseTrustLevelData + // The createProgressBar function will also safely parse numbers from these strings. + let currentTextForDisplay = req.current; + let requiredTextForDisplay = req.required; // 添加目标完成数变化的标识 let changeIndicator = ''; if (req.hasChanged) { const diff = req.changeValue; if (diff > 0) { - changeIndicator = ` ▲${diff}`; + changeIndicator = ` ${LDStatus.config.icons.ARROW_UP}${diff}`; } else if (diff < 0) { - changeIndicator = ` ▼${Math.abs(diff)}`; + changeIndicator = ` ${LDStatus.config.icons.ARROW_DOWN}${Math.abs(diff)}`; } } html += `
${name} - ${current}${changeIndicator} / ${required} + ${currentTextForDisplay}${changeIndicator} / ${requiredTextForDisplay}
- ${LDStatus.ui.createProgressBar(current, required)} + ${LDStatus.ui.createProgressBar(req.current, req.required)} `; }); @@ -551,16 +575,16 @@ ]; dailyStatsItems.forEach(item => { - const data = dailyChanges[item.key] || { day1: 0, day2: 0, trend: 0 }; + const data = dailyChanges[item.key] || { changeLast24h: 0, change24hTo48hAgo: 0, trend: 0 }; // 创建趋势指示器 let trendIndicator = ''; if (data.trend > 0) { - trendIndicator = `▲${Math.abs(data.trend)}`; + trendIndicator = `${LDStatus.config.icons.TREND_INCREASE}${Math.abs(data.trend)}`; } else if (data.trend < 0) { - trendIndicator = `▼${Math.abs(data.trend)}`; + trendIndicator = `${LDStatus.config.icons.TREND_DECREASE}${Math.abs(data.trend)}`; } else { - trendIndicator = `0`; + trendIndicator = `${LDStatus.config.icons.TREND_STABLE}`; } html += ` @@ -568,8 +592,8 @@ ${item.name} - ${data.day2} - ${data.day1} + ${data.change24hTo48hAgo} + ${data.changeLast24h} ${trendIndicator} @@ -655,11 +679,16 @@ }, // 解析信任级别数据 + // Purpose: Fetches HTML from the connect page, parses it to find trust level data, + // calculates changes from previously stored data (previousRequirements), + // and prepares the data object for rendering and caching. parseTrustLevelData: function(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); - // 查找信任级别区块 + // HTML Structure Assumption: + // Assumes the trust level data is within a div with classes '.bg-white.p-6.rounded-lg'. + // and contains a h2 element with text including '信任级别'. const trustLevelSection = Array.from(doc.querySelectorAll('.bg-white.p-6.rounded-lg')).find(div => { const heading = div.querySelector('h2'); return heading && heading.textContent.includes('信任级别'); @@ -671,67 +700,131 @@ } // 获取用户名和当前级别 - const heading = trustLevelSection.querySelector('h2').textContent.trim(); - const match = heading.match(/(.*) - 信任级别 (\d+) 的要求/); - const username = match ? match[1] : '未知用户'; - const targetLevel = match ? match[2] : '未知'; + let username = '未知用户'; + let targetLevel = '未知'; + const headingElement = trustLevelSection.querySelector('h2'); + + if (headingElement && headingElement.textContent) { + const headingText = headingElement.textContent.trim(); + const match = headingText.match(/(.*) - 信任级别 (\d+) 的要求/); + if (match && match[1] && match[2]) { + username = match[1]; + targetLevel = match[2]; + } else { + console.warn(`LDStatus: Could not parse username and targetLevel from heading: "${headingText}". Using defaults.`); + LDStatus.vars.content.innerHTML = '
数据格式错误(标题解析失败),请检查页面结构或脚本。
'; + // Depending on how critical this is, you might return or allow proceeding with defaults. + } + } else { + console.warn('LDStatus: Trust level heading element not found or has no text content.'); + LDStatus.vars.content.innerHTML = '
数据格式错误(未找到标题),请检查页面结构或脚本。
'; + return; // Stop if critical heading info is missing + } - // 获取表格数据 - const tableRows = trustLevelSection.querySelectorAll('table tr'); + // HTML Structure Assumption: + // Assumes requirements are listed in a within the trustLevelSection. + // Each requirement is a , with
elements for name, current progress, and required progress. + const tableElement = trustLevelSection.querySelector('table'); + if (!tableElement) { + console.warn('LDStatus: Trust level table element not found.'); + LDStatus.vars.content.innerHTML = '
数据格式错误(未找到表格),请检查页面结构或脚本。
'; + return; // Stop if table is missing + } + const tableRows = tableElement.querySelectorAll('tr'); const requirements = []; for (let i = 1; i < tableRows.length; i++) { // 跳过表头 const row = tableRows[i]; const cells = row.querySelectorAll('td'); - if (cells.length >= 3) { + // Ensure cells and their textContent are valid + if (cells.length >= 3 && + cells[0] && cells[0].textContent && + cells[1] && cells[1].textContent && + cells[2] && cells[2].textContent) { + const name = cells[0].textContent.trim(); - const current = cells[1].textContent.trim(); - const required = cells[2].textContent.trim(); - const isSuccess = cells[1].classList.contains('text-green-500'); - - // 提取当前完成数的数字部分 - const currentMatch = current.match(/(\d+)/); - const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0; - // 查找上一次的数据记录 + const currentText = cells[1].textContent.trim(); // Keep original text for display + const requiredText = cells[2].textContent.trim(); // Keep original text for display + const isSuccess = cells[1].classList ? cells[1].classList.contains('text-green-500') : false; + + // Robustly extract current value as a number for logic + let currentValue = 0; + const currentNumMatch = currentText.match(/(\d+)/); + if (currentNumMatch && currentNumMatch[0]) { + currentValue = parseInt(currentNumMatch[0], 10); + } else { + console.warn(`LDStatus: Could not parse current value number from: "${currentText}" for item "${name}"`); + } + + // (Optional: Robustly extract required value as a number if needed for logic later) + // let requiredValue = 0; + // const requiredNumMatch = requiredText.match(/(\d+)/); + // if (requiredNumMatch && requiredNumMatch[0]) { + // requiredValue = parseInt(requiredNumMatch[0], 10); + // } else { + // console.warn(`LDStatus: Could not parse required value number from: "${requiredText}" for item "${name}"`); + // } + + // `previousRequirements` Usage: + // `LDStatus.vars.previousRequirements` stores the full `requirements` array from the *last successful parse*. + // This allows comparison to detect changes in `currentValue` for each requirement. let changeValue = 0; let hasChanged = false; if (LDStatus.vars.previousRequirements.length > 0) { const prevReq = LDStatus.vars.previousRequirements.find(pr => pr.name === name); if (prevReq) { - // 如果完成数有变化,更新变化值 + // If current numeric value differs from the previous one, calculate the new change. if (currentValue !== prevReq.currentValue) { changeValue = currentValue - prevReq.currentValue; hasChanged = true; } else if (prevReq.changeValue) { - // 如果完成数没有变化,但之前有变化值,保留之前的变化值 + // Persisted Change Logic: + // If current value matches previous, but previous had a change indicator (non-zero changeValue), + // persist that indicator (changeValue and hasChanged flag). + // This aligns with the feature: "Even if the value does not change after refresh, the indicator will be preserved." changeValue = prevReq.changeValue; hasChanged = true; } + // If currentValue matches prevReq.currentValue AND prevReq.changeValue was 0, + // then changeValue remains 0 and hasChanged remains false (no new change, no persisted change). } } requirements.push({ name, - current, - required, + current: currentText, // Use original text for display + required: requiredText, // Use original text for display isSuccess, - currentValue, - changeValue, // 变化值 - hasChanged // 是否有变化 + currentValue, // Parsed number for logic + changeValue, + hasChanged }); + } else { + console.warn(`LDStatus: Skipping table row at index ${i} due to insufficient cells or missing text content. Cells found: ${cells.length}.`); } } // 获取总体结果 - const resultText = trustLevelSection.querySelector('p.text-red-500, p.text-green-500'); - const isMeetingRequirements = resultText ? !resultText.classList.contains('text-red-500') : false; + const resultTextElement = trustLevelSection.querySelector('p.text-red-500, p.text-green-500'); + let isMeetingRequirements = false; // Default to false + + if (resultTextElement && resultTextElement.classList) { + isMeetingRequirements = !resultTextElement.classList.contains('text-red-500'); + } else { + console.warn('LDStatus: Result text (overall status) element not found or classList is not available. Defaulting to "not meeting requirements".'); + // Optionally, inform the user in the panel if this is critical + // LDStatus.ui.showNotice("无法确定总体状态", "warn"); + } // 存储48小时内的数据变化 - const dailyChanges = this.saveDailyStats(requirements); + const dailyChanges = this.saveDailyStats(requirements); // This calculates and stores daily activity. - // 缓存数据,以备网络问题时使用 + // Data Caching: + // The full parsed data (including calculated changes and daily activity) is cached to GM_setValue. + // This allows the panel to display the last known data if the user is offline + // or if a subsequent fetch from connect.linux.do fails. GM_setValue(LDStatus.config.storageKeys.lastData, { username, targetLevel, @@ -744,34 +837,46 @@ // 渲染数据 LDStatus.ui.renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges); - // 保存当前数据作为下次比较的基准 + // `previousRequirements` Update: + // Save the current set of requirements (with their currentValues, changeValues, and hasChanged flags) + // to `LDStatus.vars.previousRequirements` for the next fetch cycle's comparison. + // A shallow copy is made to prevent modifications to the rendered data from affecting the next comparison. LDStatus.vars.previousRequirements = [...requirements]; }, // 存储48小时内的数据变化 saveDailyStats: function(requirements) { const statsToTrack = LDStatus.config.statsToTrack; - - // 获取当前时间 const now = new Date().getTime(); + const twoDaysAgo = now - 48 * 60 * 60 * 1000; - // 从 localStorage 中获取已存储的数据 - let dailyStats = JSON.parse(localStorage.getItem('ld_daily_stats') || '[]'); + // 1. Load Existing Data + let allDailyStats = []; + try { + const storedStats = localStorage.getItem('ld_daily_stats'); + if (storedStats) { + allDailyStats = JSON.parse(storedStats); + if (!Array.isArray(allDailyStats)) { // Basic validation + console.warn("LDStatus: ld_daily_stats from localStorage was not an array. Resetting."); + allDailyStats = []; + } + } + } catch (e) { + console.error("LDStatus: Error parsing ld_daily_stats from localStorage:", e); + allDailyStats = []; // Start fresh if parsing fails + } - // 删除超过48小时的数据 - const twoDaysAgo = now - 48 * 60 * 60 * 1000; - dailyStats = dailyStats.filter(item => item.timestamp > twoDaysAgo); + // 2. Filter Old Data + allDailyStats = allDailyStats.filter(item => item.timestamp > twoDaysAgo); - // 对于每个要跟踪的数据项,找到当前值并添加到历史记录中 + // 3. Add New Data statsToTrack.forEach(statName => { const req = requirements.find(r => r.name === statName); if (req) { - // 提取数字值 - const currentMatch = req.current.match(/(\d+)/); - const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0; + // req.currentValue is already parsed as a number in parseTrustLevelData + const currentValue = req.currentValue; - // 添加新的数据点 - dailyStats.push({ + allDailyStats.push({ name: statName, value: currentValue, timestamp: now @@ -779,13 +884,14 @@ } }); - // 清理过量的历史数据 - this.cleanupStorage(dailyStats); + // 4. Clean Up/Limit (ensure cleanupStorage returns the cleaned array) + allDailyStats = this.cleanupStorage(allDailyStats); // cleanupStorage sorts and slices - // 将更新后的数据保存回 localStorage - localStorage.setItem('ld_daily_stats', JSON.stringify(dailyStats)); + // 5. Save Data + localStorage.setItem('ld_daily_stats', JSON.stringify(allDailyStats)); - return this.calculateDailyChanges(dailyStats); + // Return the result of calculateDailyChanges based on the new allDailyStats + return this.calculateDailyChanges(allDailyStats); }, // 清理过量的存储数据 @@ -793,63 +899,79 @@ const maxItems = LDStatus.config.maxStorageItems; if (stats.length > maxItems) { - // 按时间戳排序并只保留最新的数据 + // 按时间戳排序并只保留最新的数据 (descending to keep newest) stats.sort((a, b) => b.timestamp - a.timestamp); - return stats.slice(0, maxItems); + return stats.slice(0, maxItems); // Important: it returns the sliced array } - - return stats; + return stats; // Return the original array if not over limit }, // 计算近两天内的变化量 + // Purpose: Calculates the net change in tracked statistics over two distinct 24-hour periods: + // 1. The most recent 24 hours (Last 24h). + // 2. The 24-hour period immediately preceding the "Last 24h" (24h-48h Ago). + // It also calculates the 'trend', which is the difference between these two net changes. + // Input: dailyStats - Array of objects: { name: string, value: number, timestamp: number } + // Output: An object where keys are statNames and values are { changeLast24h, change24hTo48hAgo, trend }. calculateDailyChanges: function(dailyStats) { const statsToTrack = LDStatus.config.statsToTrack; const result = {}; const now = new Date().getTime(); + + // Time Period Definitions: + // `oneDayAgo` marks the boundary between "Last 24h" and "24h-48h Ago". + // `twoDaysAgo` marks the older boundary for the "24h-48h Ago" period. const oneDayAgo = now - 24 * 60 * 60 * 1000; const twoDaysAgo = now - 48 * 60 * 60 * 1000; - // 对于每个要跟踪的数据项,计算两天内的变化 statsToTrack.forEach(statName => { - // 过滤出当前数据项的所有记录,并按时间戳排序 + // Filter records for the current stat and sort them by timestamp (ascending). + // This makes it easier to find the earliest and latest records in a period. const statRecords = dailyStats .filter(item => item.name === statName) .sort((a, b) => a.timestamp - b.timestamp); - // 初始化结果对象结构 result[statName] = { - day1: 0, // 最近24小时 - day2: 0, // 24-48小时 - trend: 0 // 趋势:day1 - day2 + changeLast24h: 0, // Net change in value over the most recent 24 hours. + change24hTo48hAgo: 0, // Net change in value over the 24-hour period before the most recent one. + trend: 0 // Difference: changeLast24h - change24hTo48hAgo. }; if (statRecords.length >= 2) { - // 找出最新记录和其前面两个时间段的记录 + // Record Identification: + // `newest`: The absolute latest record available for this statistic. const newest = statRecords[statRecords.length - 1]; - // 找最近24小时内最早的记录 - const oldestDay1 = statRecords.filter(item => item.timestamp > oneDayAgo)[0]; + // `oldestLast24h`: The earliest record that falls within the "Last 24h" window (timestamp > oneDayAgo). + // Used as the starting point to calculate change during the most recent 24 hours. + const oldestLast24h = statRecords.filter(item => item.timestamp > oneDayAgo)[0]; - // 找24-48小时内最早的记录和最新的记录 - const recordsDay2 = statRecords.filter(item => + // Records for the "24h-48h Ago" period. + const records24hTo48hAgo = statRecords.filter(item => item.timestamp <= oneDayAgo && item.timestamp > twoDaysAgo); - const oldestDay2 = recordsDay2.length > 0 ? recordsDay2[0] : null; - const newestDay2 = recordsDay2.length > 0 ? - recordsDay2[recordsDay2.length - 1] : null; - - // 计算最近24小时的变化 - if (oldestDay1) { - result[statName].day1 = newest.value - oldestDay1.value; + // `oldest24hTo48hAgo`: Earliest record in the "24h-48h Ago" window. + // `newest24hTo48hAgo`: Latest record in the "24h-48h Ago" window. + // These are used to calculate the net change *within* that specific 24-hour slot. + const oldest24hTo48hAgo = records24hTo48hAgo.length > 0 ? records24hTo48hAgo[0] : null; + const newest24hTo48hAgo = records24hTo48hAgo.length > 0 ? + records24hTo48hAgo[records24hTo48hAgo.length - 1] : null; + + // Change Calculation: + // `changeLast24h`: Net change from the start of the "Last 24h" period to the `newest` record. + if (oldestLast24h) { + result[statName].changeLast24h = newest.value - oldestLast24h.value; } - // 计算24-48小时的变化 - if (oldestDay2 && newestDay2) { - result[statName].day2 = newestDay2.value - oldestDay2.value; + // `change24hTo48hAgo`: Net change from the start to the end of the "24h-48h Ago" period. + if (oldest24hTo48hAgo && newest24hTo48hAgo) { + result[statName].change24hTo48hAgo = newest24hTo48hAgo.value - oldest24hTo48hAgo.value; } - // 计算趋势(今天和昨天的变化差异) - result[statName].trend = result[statName].day1 - result[statName].day2; + // `trend`: Difference between the net change in the last 24h and the net change in the 24h before that. + // A positive trend means activity is increasing more (or decreasing less) in the most recent 24h + // compared to the prior 24h period. + result[statName].trend = result[statName].changeLast24h - result[statName].change24hTo48hAgo; } }); @@ -862,7 +984,7 @@ const updateBtn = LDStatus.vars.updateBtn; // 显示正在检查的状态 - updateBtn.textContent = '⌛'; // 沙漏图标 + updateBtn.textContent = LDStatus.config.icons.LOADING; updateBtn.title = '正在检查更新...'; GM_xmlhttpRequest({ @@ -879,7 +1001,7 @@ // 比较版本 if (remoteVersion > scriptVersion) { // 有新版本 - updateBtn.textContent = '⚠️'; // 警告图标 + updateBtn.textContent = LDStatus.config.icons.UPDATE_AVAILABLE; updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`; updateBtn.style.color = 'var(--ld-increase-color)'; // 黄色 @@ -889,42 +1011,40 @@ }; } else { // 已是最新版本 - updateBtn.textContent = '✔'; // 勾选图标 - updateBtn.title = '已是最新版本'; + updateBtn.textContent = LDStatus.config.icons.UP_TO_DATE; + updateBtn.title = '已是最新版本,点击再次检查'; // Updated title for clarity updateBtn.style.color = 'var(--ld-success-color)'; // 绿色 - - // 3秒后恢复原样式 - setTimeout(() => { - updateBtn.textContent = '🔎'; // 放大镜图标 - updateBtn.title = '检查更新'; - updateBtn.style.color = ''; - updateBtn.onclick = LDStatus.events.onUpdateBtnClick; - }, 3000); + // Ensure onclick allows re-checking + updateBtn.onclick = LDStatus.events.onUpdateBtnClick; + // Removed setTimeout to make the "up-to-date" state persistent } } else { - LDStatus.data.handleUpdateError(); + // This case means versionMatch failed, treat as an error in parsing response + console.warn("LDStatus: Could not parse version from update check response."); + LDStatus.data.handleUpdateError(); // Call existing error handler } } else { + // HTTP error + console.warn(`LDStatus: Update check HTTP error: ${response.status}`); LDStatus.data.handleUpdateError(); } }, - onerror: LDStatus.data.handleUpdateError + onerror: function(error) { // Network error + console.warn("LDStatus: Update check network error.", error); + LDStatus.data.handleUpdateError(); + } }); }, // 处理更新检查错误 handleUpdateError: function() { const updateBtn = LDStatus.vars.updateBtn; - updateBtn.textContent = '❌'; // 错误图标 - updateBtn.title = '检查更新失败,请稍后再试'; + updateBtn.textContent = LDStatus.config.icons.ERROR; + updateBtn.title = '检查更新失败,点击再次检查'; // Updated title for clarity updateBtn.style.color = 'var(--ld-fail-color)'; // 红色 - - // 3秒后恢复原样式 - setTimeout(() => { - updateBtn.textContent = '🔎'; // 放大镜图标 - updateBtn.title = '检查更新'; - updateBtn.style.color = ''; - }, 3000); + // Ensure onclick allows re-checking + updateBtn.onclick = LDStatus.events.onUpdateBtnClick; + // Removed setTimeout to make the error state persistent until user interaction } }, @@ -1003,7 +1123,9 @@ onToggleBtnClick: function() { const panel = LDStatus.vars.panel; panel.classList.toggle('ld-collapsed'); - LDStatus.vars.toggleBtn.textContent = panel.classList.contains('ld-collapsed') ? '▶' : '◀'; + LDStatus.vars.toggleBtn.textContent = panel.classList.contains('ld-collapsed') + ? LDStatus.config.icons.EXPAND + : LDStatus.config.icons.COLLAPSE; // 保存折叠状态 LDStatus.storage.savePanelCollapsedState(); @@ -1095,10 +1217,10 @@ if (isCollapsed) { LDStatus.vars.panel.classList.add('ld-collapsed'); - LDStatus.vars.toggleBtn.textContent = '▶'; + LDStatus.vars.toggleBtn.textContent = LDStatus.config.icons.EXPAND; } else { LDStatus.vars.panel.classList.remove('ld-collapsed'); - LDStatus.vars.toggleBtn.textContent = '◀'; + LDStatus.vars.toggleBtn.textContent = LDStatus.config.icons.COLLAPSE; } } },