├── LDStatus.user.js └── README.md /LDStatus.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name LDStatus 3 | // @namespace http://tampermonkey.net/ 4 | // @version 1.11 5 | // @description 在 Linux.do 页面显示信任级别进度 6 | // @author 1e0n 7 | // @match https://linux.do/* 8 | // @grant GM_xmlhttpRequest 9 | // @grant GM_setValue 10 | // @grant GM_getValue 11 | // @grant GM_info 12 | // @connect connect.linux.do 13 | // @connect github.com 14 | // @connect raw.githubusercontent.com 15 | // @updateURL https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js 16 | // @downloadURL https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js 17 | // ==/UserScript== 18 | 19 | (function() { 20 | 'use strict'; 21 | 22 | // 创建样式 - 使用更特定的选择器以避免影响帖子界面的按钮 23 | const style = document.createElement('style'); 24 | style.textContent = ` 25 | /* 深色主题 */ 26 | #ld-trust-level-panel.ld-dark-theme { 27 | background-color: #2d3748; 28 | color: #e2e8f0; 29 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); 30 | } 31 | 32 | #ld-trust-level-panel.ld-dark-theme #ld-trust-level-header { 33 | background-color: #1a202c; 34 | color: white; 35 | } 36 | 37 | #ld-trust-level-panel.ld-dark-theme .ld-trust-level-item.ld-success .ld-value { 38 | color: #68d391; 39 | } 40 | 41 | #ld-trust-level-panel.ld-dark-theme .ld-trust-level-item.ld-fail .ld-value { 42 | color: #fc8181; 43 | } 44 | 45 | #ld-trust-level-panel.ld-dark-theme .ld-loading { 46 | color: #a0aec0; 47 | } 48 | 49 | #ld-trust-level-panel.ld-dark-theme .ld-daily-stats-title { 50 | color: #a0aec0; 51 | } 52 | 53 | #ld-trust-level-panel.ld-dark-theme .ld-daily-stats-item .ld-value { 54 | color: #68d391; 55 | } 56 | 57 | #ld-trust-level-panel.ld-dark-theme .ld-version { 58 | color: #a0aec0; 59 | } 60 | 61 | /* 亮色主题 - 提高对比度 */ 62 | #ld-trust-level-panel.ld-light-theme { 63 | background-color: #ffffff; 64 | color: #1a202c; 65 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); 66 | border: 1px solid #e2e8f0; 67 | } 68 | 69 | #ld-trust-level-panel.ld-light-theme #ld-trust-level-header { 70 | background-color: #3182ce; /* 更深的蓝色 */ 71 | color: #ffffff; 72 | border-bottom: 1px solid #2c5282; /* 添加底部边框 */ 73 | } 74 | 75 | #ld-trust-level-panel.ld-light-theme .ld-trust-level-item.ld-success .ld-value { 76 | color: #276749; /* 更深的绿色 */ 77 | font-weight: bold; 78 | } 79 | 80 | #ld-trust-level-panel.ld-light-theme .ld-trust-level-item.ld-fail .ld-value { 81 | color: #c53030; 82 | font-weight: bold; 83 | } 84 | 85 | /* 亮色主题下的文本颜色 */ 86 | #ld-trust-level-panel.ld-light-theme .ld-name { 87 | color: #2d3748; /* 深灰色 */ 88 | } 89 | 90 | #ld-trust-level-panel.ld-light-theme .ld-loading { 91 | color: #4a5568; 92 | } 93 | 94 | #ld-trust-level-panel.ld-light-theme .ld-daily-stats-title { 95 | color: #4a5568; 96 | font-weight: bold; 97 | } 98 | 99 | #ld-trust-level-panel.ld-light-theme .ld-daily-stats-item .ld-value { 100 | color: #2c7a4b; 101 | font-weight: bold; 102 | } 103 | 104 | #ld-trust-level-panel.ld-light-theme .ld-version { 105 | color: #e2e8f0; 106 | } 107 | 108 | /* 共用样式 */ 109 | #ld-trust-level-panel { 110 | position: fixed; 111 | left: 10px; 112 | top: 100px; 113 | width: 210px; 114 | border-radius: 8px; 115 | z-index: 9999; 116 | font-family: Arial, sans-serif; 117 | transition: all 0.3s ease; 118 | overflow: hidden; 119 | font-size: 12px; 120 | } 121 | 122 | #ld-trust-level-header { 123 | padding: 8px 10px; 124 | cursor: move; 125 | display: flex; 126 | justify-content: space-between; 127 | align-items: center; 128 | user-select: none; 129 | } 130 | 131 | .ld-header-content { 132 | display: flex; 133 | width: 100%; 134 | align-items: center; 135 | justify-content: space-between; 136 | white-space: nowrap; 137 | } 138 | 139 | .ld-header-content > span:first-child { 140 | margin-right: auto; 141 | font-weight: bold; 142 | } 143 | 144 | #ld-trust-level-content { 145 | padding: 10px; 146 | max-height: none; 147 | overflow-y: visible; 148 | } 149 | 150 | .ld-trust-level-item { 151 | margin-bottom: 6px; 152 | display: flex; 153 | white-space: nowrap; 154 | width: 100%; 155 | justify-content: space-between; 156 | } 157 | 158 | .ld-trust-level-item .ld-name { 159 | flex: 0 1 auto; 160 | overflow: hidden; 161 | text-overflow: ellipsis; 162 | max-width: 60%; 163 | } 164 | 165 | .ld-trust-level-item .ld-value { 166 | font-weight: bold; 167 | flex: 0 0 auto; 168 | text-align: right; 169 | min-width: 70px; 170 | } 171 | 172 | /* 这些样式已移动到主题特定样式中 */ 173 | 174 | .ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn { 175 | background: none; 176 | border: none; 177 | color: white; 178 | cursor: pointer; 179 | font-size: 14px; 180 | margin-left: 5px; 181 | } 182 | 183 | .ld-version { 184 | font-size: 10px; 185 | color: #a0aec0; 186 | margin-left: 5px; 187 | font-weight: normal; 188 | } 189 | 190 | .ld-collapsed { 191 | width: 40px !important; 192 | height: 40px !important; 193 | min-width: 40px !important; 194 | max-width: 40px !important; 195 | border-radius: 8px; 196 | overflow: hidden; 197 | transform: none !important; 198 | } 199 | 200 | .ld-collapsed #ld-trust-level-header { 201 | justify-content: center; 202 | width: 40px !important; 203 | height: 40px !important; 204 | min-width: 40px !important; 205 | max-width: 40px !important; 206 | padding: 0; 207 | display: flex; 208 | align-items: center; 209 | } 210 | 211 | .ld-collapsed #ld-trust-level-header > div { 212 | justify-content: center; 213 | width: 100%; 214 | height: 100%; 215 | } 216 | 217 | .ld-collapsed #ld-trust-level-content { 218 | display: none !important; 219 | } 220 | 221 | .ld-collapsed .ld-header-content > span, 222 | .ld-collapsed .ld-refresh-btn, 223 | .ld-collapsed .ld-update-btn, 224 | .ld-collapsed .ld-theme-btn, 225 | .ld-collapsed .ld-version { 226 | display: none !important; 227 | } 228 | 229 | .ld-collapsed .ld-toggle-btn { 230 | margin: 0; 231 | font-size: 16px; 232 | display: flex; 233 | justify-content: center; 234 | align-items: center; 235 | width: 100%; 236 | height: 100%; 237 | } 238 | 239 | .ld-loading { 240 | text-align: center; 241 | padding: 10px; 242 | } 243 | 244 | /* 深色主题下的变化指示器 */ 245 | .ld-dark-theme .ld-increase { 246 | color: #ffd700; /* 黄色 */ 247 | } 248 | 249 | .ld-dark-theme .ld-decrease { 250 | color: #4299e1; /* 蓝色 */ 251 | } 252 | 253 | /* 亮色主题下的变化指示器 */ 254 | .ld-light-theme .ld-increase { 255 | color: #d69e2e; /* 深黄色 */ 256 | font-weight: bold; 257 | } 258 | 259 | .ld-light-theme .ld-decrease { 260 | color: #2b6cb0; /* 深蓝色 */ 261 | font-weight: bold; 262 | } 263 | 264 | /* 所有主题下的活动数据区域 */ 265 | .ld-daily-stats { 266 | margin-top: 10px; 267 | font-size: 11px; 268 | } 269 | 270 | /* 深色主题下的分隔线 */ 271 | .ld-dark-theme .ld-daily-stats { 272 | border-top: 1px solid #4a5568; 273 | padding-top: 10px; 274 | } 275 | 276 | /* 亮色主题下的分隔线 */ 277 | .ld-light-theme .ld-daily-stats { 278 | border-top: 1px solid #cbd5e0; 279 | padding-top: 10px; 280 | } 281 | 282 | .ld-daily-stats-title { 283 | font-weight: bold; 284 | margin-bottom: 5px; 285 | } 286 | 287 | .ld-daily-stats-item { 288 | display: flex; 289 | justify-content: space-between; 290 | margin-bottom: 4px; 291 | } 292 | 293 | .ld-daily-stats-item .ld-name { 294 | flex: 0 1 auto; 295 | color: inherit; 296 | } 297 | 298 | .ld-daily-stats-item .ld-value { 299 | flex: 0 0 auto; 300 | font-weight: bold; 301 | color: inherit; 302 | } 303 | 304 | /* 添加两天数据的样式 */ 305 | .ld-dual-stats { 306 | display: flex; 307 | justify-content: flex-end; 308 | gap: 5px; 309 | min-width: 70px; 310 | text-align: right; 311 | } 312 | 313 | .ld-day-stat { 314 | min-width: 25px; 315 | width: 25px; 316 | text-align: right; 317 | display: inline-block; 318 | } 319 | 320 | .ld-day1 { 321 | color: #68d391; /* 跟上部一致的绿色 */ 322 | } 323 | 324 | .ld-day2 { 325 | color: #a0aec0; /* 跟上部一致的灰色 */ 326 | } 327 | 328 | .ld-light-theme .ld-day1 { 329 | color: #276749; /* 浅色主题下与主面板绿色一致 */ 330 | } 331 | 332 | .ld-light-theme .ld-day2 { 333 | color: #2d3748; /* 浅色主题下更深的灰色 */ 334 | } 335 | 336 | .ld-dark-theme .ld-day2 { 337 | color: #cbd5e1; /* 深色主题下更亮的灰色,增强可读性 */ 338 | } 339 | 340 | .ld-trend-indicator { 341 | margin-left: 2px; 342 | display: inline-block; 343 | min-width: 25px; 344 | width: 25px; 345 | text-align: left; 346 | } 347 | 348 | .ld-stats-header { 349 | display: flex; 350 | justify-content: space-between; 351 | margin-bottom: 6px; 352 | font-size: 10px; 353 | color: inherit; 354 | } 355 | 356 | .ld-light-theme .ld-stats-header { 357 | color: #2d3748; 358 | } 359 | 360 | .ld-dark-theme .ld-stats-header { 361 | color: #e2e8f0; 362 | } 363 | 364 | .ld-stats-header-cols { 365 | display: flex; 366 | gap: 5px; 367 | min-width: 70px; 368 | justify-content: flex-end; 369 | } 370 | 371 | .ld-stats-header-col { 372 | min-width: 25px; 373 | width: 25px; 374 | text-align: center; 375 | } 376 | 377 | .ld-stats-header-trend { 378 | min-width: 25px; 379 | width: 25px; 380 | text-align: center; 381 | } 382 | `; 383 | document.head.appendChild(style); 384 | 385 | // 定义存储键 386 | const STORAGE_KEY_POSITION = 'ld_panel_position'; 387 | const STORAGE_KEY_COLLAPSED = 'ld_panel_collapsed'; 388 | const STORAGE_KEY_THEME = 'ld_panel_theme'; 389 | 390 | // 创建面板 391 | const panel = document.createElement('div'); 392 | panel.id = 'ld-trust-level-panel'; 393 | 394 | // 设置默认主题 395 | const currentTheme = GM_getValue(STORAGE_KEY_THEME, 'dark'); 396 | panel.classList.add(currentTheme === 'dark' ? 'ld-dark-theme' : 'ld-light-theme'); 397 | 398 | // 获取脚本版本号 399 | const scriptVersion = GM_info.script.version; 400 | 401 | // 创建面板头部 402 | const header = document.createElement('div'); 403 | header.id = 'ld-trust-level-header'; 404 | header.innerHTML = ` 405 |
406 | Status 407 | v${scriptVersion} 408 | 409 | 410 | 411 | 412 |
413 | `; 414 | 415 | // 创建内容区域 416 | const content = document.createElement('div'); 417 | content.id = 'ld-trust-level-content'; 418 | content.innerHTML = '
加载中...
'; 419 | 420 | // 组装面板 421 | panel.appendChild(header); 422 | panel.appendChild(content); 423 | document.body.appendChild(panel); 424 | 425 | // 保存窗口位置的函数 426 | function savePanelPosition() { 427 | const transform = window.getComputedStyle(panel).transform; 428 | if (transform && transform !== 'none') { 429 | const matrix = new DOMMatrix(transform); 430 | GM_setValue(STORAGE_KEY_POSITION, { x: matrix.e, y: matrix.f }); 431 | } 432 | } 433 | 434 | // 保存窗口折叠状态的函数 435 | function savePanelCollapsedState() { 436 | GM_setValue(STORAGE_KEY_COLLAPSED, panel.classList.contains('ld-collapsed')); 437 | } 438 | 439 | // 恢复窗口状态 440 | function restorePanelState() { 441 | // 恢复折叠状态 442 | const isCollapsed = GM_getValue(STORAGE_KEY_COLLAPSED, false); 443 | if (isCollapsed) { 444 | panel.classList.add('ld-collapsed'); 445 | toggleBtn.textContent = '▶'; // 右箭头 446 | } else { 447 | panel.classList.remove('ld-collapsed'); 448 | toggleBtn.textContent = '◀'; // 左箭头 449 | } 450 | 451 | // 恢复位置 452 | const position = GM_getValue(STORAGE_KEY_POSITION, null); 453 | if (position) { 454 | panel.style.transform = `translate(${position.x}px, ${position.y}px)`; 455 | } 456 | } 457 | 458 | // 拖动功能 459 | let isDragging = false; 460 | let lastX, lastY; 461 | 462 | header.addEventListener('mousedown', (e) => { 463 | if (panel.classList.contains('ld-collapsed')) return; 464 | 465 | isDragging = true; 466 | lastX = e.clientX; 467 | lastY = e.clientY; 468 | 469 | // 添加拖动时的样式 470 | panel.style.transition = 'none'; 471 | document.body.style.userSelect = 'none'; 472 | }); 473 | 474 | document.addEventListener('mousemove', (e) => { 475 | if (!isDragging) return; 476 | 477 | // 使用 transform 而不是改变 left/top 属性,性能更好 478 | const dx = e.clientX - lastX; 479 | const dy = e.clientY - lastY; 480 | 481 | const currentTransform = window.getComputedStyle(panel).transform; 482 | const matrix = new DOMMatrix(currentTransform === 'none' ? '' : currentTransform); 483 | 484 | const newX = matrix.e + dx; 485 | const newY = matrix.f + dy; 486 | 487 | panel.style.transform = `translate(${newX}px, ${newY}px)`; 488 | 489 | lastX = e.clientX; 490 | lastY = e.clientY; 491 | }); 492 | 493 | document.addEventListener('mouseup', () => { 494 | if (!isDragging) return; 495 | 496 | isDragging = false; 497 | panel.style.transition = ''; 498 | document.body.style.userSelect = ''; 499 | 500 | // 保存窗口位置 501 | savePanelPosition(); 502 | }); 503 | 504 | // 展开/收起功能 505 | const toggleBtn = header.querySelector('.ld-toggle-btn'); 506 | toggleBtn.addEventListener('click', () => { 507 | panel.classList.toggle('ld-collapsed'); 508 | toggleBtn.textContent = panel.classList.contains('ld-collapsed') ? '▶' : '◀'; 509 | 510 | // 保存折叠状态 511 | savePanelCollapsedState(); 512 | }); 513 | 514 | // 刷新按钮 515 | const refreshBtn = header.querySelector('.ld-refresh-btn'); 516 | refreshBtn.addEventListener('click', fetchTrustLevelData); 517 | 518 | // 检查更新按钮 519 | const updateBtn = header.querySelector('.ld-update-btn'); 520 | updateBtn.addEventListener('click', checkForUpdates); 521 | 522 | // 主题切换按钮 523 | const themeBtn = header.querySelector('.ld-theme-btn'); 524 | themeBtn.addEventListener('click', toggleTheme); 525 | 526 | // 更新主题按钮图标 527 | updateThemeButtonIcon(); 528 | 529 | // 切换主题函数 530 | function toggleTheme() { 531 | const isDarkTheme = panel.classList.contains('ld-dark-theme'); 532 | 533 | // 切换主题类 534 | panel.classList.remove(isDarkTheme ? 'ld-dark-theme' : 'ld-light-theme'); 535 | panel.classList.add(isDarkTheme ? 'ld-light-theme' : 'ld-dark-theme'); 536 | 537 | // 保存主题设置 538 | GM_setValue(STORAGE_KEY_THEME, isDarkTheme ? 'light' : 'dark'); 539 | 540 | // 更新主题按钮图标 541 | updateThemeButtonIcon(); 542 | } 543 | 544 | // 更新主题按钮图标 545 | function updateThemeButtonIcon() { 546 | const isDarkTheme = panel.classList.contains('ld-dark-theme'); 547 | themeBtn.textContent = isDarkTheme ? '🌙' : '☀️'; // 月亮或太阳图标 548 | themeBtn.title = isDarkTheme ? '切换为亮色主题' : '切换为深色主题'; 549 | 550 | // 在亮色主题下调整按钮颜色 551 | if (!isDarkTheme) { 552 | document.querySelectorAll('.ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn').forEach(btn => { 553 | btn.style.color = 'white'; // 亮色主题下按钮使用白色,因为标题栏是蓝色 554 | btn.style.textShadow = '0 0 1px rgba(0,0,0,0.3)'; // 添加文字阴影增强可读性 555 | }); 556 | } else { 557 | document.querySelectorAll('.ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn').forEach(btn => { 558 | btn.style.color = 'white'; 559 | btn.style.textShadow = 'none'; 560 | }); 561 | } 562 | } 563 | 564 | // 检查脚本更新 565 | function checkForUpdates() { 566 | const updateURL = 'https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js'; 567 | 568 | // 显示正在检查的状态 569 | updateBtn.textContent = '⌛'; // 沙漏图标 570 | updateBtn.title = '正在检查更新...'; 571 | 572 | GM_xmlhttpRequest({ 573 | method: 'GET', 574 | url: updateURL, 575 | onload: function(response) { 576 | if (response.status === 200) { 577 | // 提取远程脚本的版本号 578 | const versionMatch = response.responseText.match(/@version\s+([\d\.]+)/); 579 | if (versionMatch && versionMatch[1]) { 580 | const remoteVersion = versionMatch[1]; 581 | 582 | // 比较版本 583 | if (remoteVersion > scriptVersion) { 584 | // 有新版本 585 | updateBtn.textContent = '⚠️'; // 警告图标 586 | updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`; 587 | updateBtn.style.color = '#ffd700'; // 黄色 588 | 589 | // 点击按钮跳转到更新页面 590 | updateBtn.onclick = function() { 591 | window.open(updateURL, '_blank'); 592 | }; 593 | } else { 594 | // 已是最新版本 595 | updateBtn.textContent = '✔'; // 勾选图标 596 | updateBtn.title = '已是最新版本'; 597 | updateBtn.style.color = '#68d391'; // 绿色 598 | 599 | // 3秒后恢复原样式 600 | setTimeout(() => { 601 | updateBtn.textContent = '🔎'; // 放大镜图标 602 | updateBtn.title = '检查更新'; 603 | updateBtn.style.color = 'white'; 604 | updateBtn.onclick = checkForUpdates; 605 | }, 3000); 606 | } 607 | } else { 608 | handleUpdateError(); 609 | } 610 | } else { 611 | handleUpdateError(); 612 | } 613 | }, 614 | onerror: handleUpdateError 615 | }); 616 | 617 | // 处理更新检查错误 618 | function handleUpdateError() { 619 | updateBtn.textContent = '❌'; // 错误图标 620 | updateBtn.title = '检查更新失败,请稍后再试'; 621 | updateBtn.style.color = '#fc8181'; // 红色 622 | 623 | // 3秒后恢复原样式 624 | setTimeout(() => { 625 | updateBtn.textContent = '🔎'; // 放大镜图标 626 | updateBtn.title = '检查更新'; 627 | updateBtn.style.color = 'white'; 628 | }, 3000); 629 | } 630 | } 631 | 632 | // 获取信任级别数据 633 | function fetchTrustLevelData() { 634 | content.innerHTML = '
加载中...
'; 635 | 636 | GM_xmlhttpRequest({ 637 | method: 'GET', 638 | url: 'https://connect.linux.do', 639 | onload: function(response) { 640 | if (response.status === 200) { 641 | parseTrustLevelData(response.responseText); 642 | } else { 643 | content.innerHTML = '
获取数据失败,请稍后再试
'; 644 | } 645 | }, 646 | onerror: function() { 647 | content.innerHTML = '
获取数据失败,请稍后再试
'; 648 | } 649 | }); 650 | } 651 | 652 | // 解析信任级别数据 653 | function parseTrustLevelData(html) { 654 | const parser = new DOMParser(); 655 | const doc = parser.parseFromString(html, 'text/html'); 656 | 657 | // 查找信任级别区块 658 | const trustLevelSection = Array.from(doc.querySelectorAll('.bg-white.p-6.rounded-lg')).find(div => { 659 | const heading = div.querySelector('h2'); 660 | return heading && heading.textContent.includes('信任级别'); 661 | }); 662 | 663 | if (!trustLevelSection) { 664 | content.innerHTML = '
未找到信任级别数据,请确保已登录
'; 665 | return; 666 | } 667 | 668 | // 获取用户名和当前级别 669 | const heading = trustLevelSection.querySelector('h2').textContent.trim(); 670 | const match = heading.match(/(.*) - 信任级别 (\d+) 的要求/); 671 | const username = match ? match[1] : '未知用户'; 672 | const targetLevel = match ? match[2] : '未知'; 673 | 674 | // 获取表格数据 675 | const tableRows = trustLevelSection.querySelectorAll('table tr'); 676 | const requirements = []; 677 | 678 | for (let i = 1; i < tableRows.length; i++) { // 跳过表头 679 | const row = tableRows[i]; 680 | const cells = row.querySelectorAll('td'); 681 | 682 | if (cells.length >= 3) { 683 | const name = cells[0].textContent.trim(); 684 | const current = cells[1].textContent.trim(); 685 | const required = cells[2].textContent.trim(); 686 | const isSuccess = cells[1].classList.contains('text-green-500'); 687 | 688 | // 提取当前完成数的数字部分 689 | const currentMatch = current.match(/(\d+)/); 690 | const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0; 691 | 692 | // 查找上一次的数据记录 693 | let changeValue = 0; 694 | let hasChanged = false; 695 | 696 | if (previousRequirements.length > 0) { 697 | const prevReq = previousRequirements.find(pr => pr.name === name); 698 | if (prevReq) { 699 | // 如果完成数有变化,更新变化值 700 | if (currentValue !== prevReq.currentValue) { 701 | changeValue = currentValue - prevReq.currentValue; 702 | hasChanged = true; 703 | } else if (prevReq.changeValue) { 704 | // 如果完成数没有变化,但之前有变化值,保留之前的变化值 705 | changeValue = prevReq.changeValue; 706 | hasChanged = true; 707 | } 708 | } 709 | } 710 | 711 | requirements.push({ 712 | name, 713 | current, 714 | required, 715 | isSuccess, 716 | currentValue, 717 | changeValue, // 变化值 718 | hasChanged // 是否有变化 719 | }); 720 | } 721 | } 722 | 723 | // 获取总体结果 724 | const resultText = trustLevelSection.querySelector('p.text-red-500, p.text-green-500'); 725 | const isMeetingRequirements = resultText ? !resultText.classList.contains('text-red-500') : false; 726 | 727 | // 存储自然日的活动数据 728 | const dailyChanges = saveDailyStats(requirements); 729 | 730 | // 渲染数据 731 | renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges); 732 | 733 | // 保存当前数据作为下次比较的基准 734 | previousRequirements = [...requirements]; 735 | } 736 | 737 | // 渲染信任级别数据 738 | function renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements, dailyChanges = {}) { 739 | let html = ` 740 |
741 | ${username} - 信任级别 ${targetLevel} 742 |
743 |
744 | ${isMeetingRequirements ? '已' : '未'}符合信任级别 ${targetLevel} 要求 745 |
746 | `; 747 | 748 | requirements.forEach(req => { 749 | // 简化项目名称 750 | let name = req.name; 751 | // 将一些常见的长名称缩短 752 | name = name.replace('已读帖子(所有时间)', '已读帖子(总)'); 753 | name = name.replace('浏览的话题(所有时间)', '浏览话题(总)'); 754 | name = name.replace('获赞:点赞用户数量', '点赞用户数'); 755 | name = name.replace('获赞:单日最高数量', '总获赞天数'); 756 | name = name.replace('被禁言(过去 6 个月)', '被禁言'); 757 | name = name.replace('被封禁(过去 6 个月)', '被封禁'); 758 | 759 | // 提取数字部分以简化显示 760 | let current = req.current; 761 | let required = req.required; 762 | 763 | // 尝试从字符串中提取数字 764 | const currentMatch = req.current.match(/(\d+)/); 765 | const requiredMatch = req.required.match(/(\d+)/); 766 | 767 | if (currentMatch) current = currentMatch[1]; 768 | if (requiredMatch) required = requiredMatch[1]; 769 | 770 | // 添加目标完成数变化的标识 771 | let changeIndicator = ''; 772 | if (req.hasChanged) { 773 | const diff = req.changeValue; 774 | if (diff > 0) { 775 | changeIndicator = ` ▲${diff}`; // 增加标识,黄色 776 | } else if (diff < 0) { 777 | changeIndicator = ` ▼${Math.abs(diff)}`; // 减少标识,蓝色 778 | } 779 | } 780 | 781 | html += ` 782 |
783 | ${name} 784 | ${current}${changeIndicator} / ${required} 785 |
786 | `; 787 | }); 788 | 789 | // 添加近期活动数据显示 790 | html += ` 791 |
792 |
近期的活动
793 |
794 | 795 | 796 | 昨天 797 | 今天 798 | 799 | 800 |
801 | `; 802 | 803 | // 添加每个数据项 804 | const dailyStatsItems = [ 805 | { name: '浏览话题', key: '浏览的话题(所有时间)' }, 806 | { name: '回复话题', key: '回复的话题' }, 807 | { name: '已读帖子', key: '已读帖子(所有时间)' }, 808 | { name: '获得点赞', key: '获赞:点赞用户数量' }, 809 | { name: '点赞帖子', key: '点赞' } 810 | ]; 811 | 812 | dailyStatsItems.forEach(item => { 813 | const data = dailyChanges[item.key] || { day1: 0, day2: 0, trend: 0 }; 814 | 815 | // 创建趋势指示器 816 | let trendIndicator = ''; 817 | if (data.trend > 0) { 818 | trendIndicator = `▲${Math.abs(data.trend)}`; 819 | } else if (data.trend < 0) { 820 | trendIndicator = `▼${Math.abs(data.trend)}`; 821 | } else { 822 | trendIndicator = `0`; 823 | } 824 | 825 | html += ` 826 |
827 | ${item.name} 828 | 829 | 830 | ${data.day2} 831 | ${data.day1} 832 | ${trendIndicator} 833 | 834 | 835 |
836 | `; 837 | }); 838 | 839 | html += `
`; 840 | 841 | content.innerHTML = html; 842 | } 843 | 844 | // 存储上一次获取的数据,用于比较变化 845 | let previousRequirements = []; 846 | 847 | // 存储自然日的活动数据 848 | function saveDailyStats(requirements) { 849 | // 定义要跟踪的数据项 850 | const statsToTrack = [ 851 | '浏览的话题(所有时间)', // 浏览话题总数 852 | '回复的话题', // 回复话题数 853 | '已读帖子(所有时间)', // 已读帖子总数 854 | '获赞:点赞用户数量', // 获赞数 855 | '点赞' // 点赞数 856 | ]; 857 | 858 | // 调试信息:输出所有数据项的名称 859 | console.log('数据项名称:', requirements.map(r => r.name)); 860 | console.log('要跟踪的数据项:', statsToTrack); 861 | 862 | // 获取当前时间和日期边界 863 | const now = new Date(); 864 | const currentTime = now.getTime(); 865 | const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); // 今天0点 866 | const yesterdayStart = todayStart - (24 * 60 * 60 * 1000); // 昨天0点 867 | 868 | // 从 localStorage 中获取已存储的数据 869 | let dailyStats = JSON.parse(localStorage.getItem('ld_daily_stats') || '[]'); 870 | 871 | // 优化存储:只保留昨天和今天的数据点,且每天只保留自然日的第一个和最后一个数据点 872 | const newDailyStats = []; 873 | 874 | // 对于每个数据项,分别处理 875 | statsToTrack.forEach(statName => { 876 | // 过滤出当前数据项的所有记录 877 | const allRecords = dailyStats.filter(item => item.name === statName); 878 | if (allRecords.length === 0) { 879 | return; // 跳过这个数据项 880 | } 881 | 882 | // 查找今天和昨天自然日范围内的最早和最晚记录 883 | const todayRecords = allRecords.filter(item => item.timestamp >= todayStart); 884 | const yesterdayRecords = allRecords.filter(item => 885 | item.timestamp >= yesterdayStart && item.timestamp < todayStart); 886 | 887 | // 处理昨天的数据:只保留最早和最晚的记录 888 | if (yesterdayRecords.length > 0) { 889 | // 查找昨天的第一条记录 890 | const firstRecord = yesterdayRecords.reduce((earliest, current) => 891 | current.timestamp < earliest.timestamp ? current : earliest, yesterdayRecords[0]); 892 | 893 | // 查找昨天的最后一条记录 894 | const lastRecord = yesterdayRecords.reduce((latest, current) => 895 | current.timestamp > latest.timestamp ? current : latest, yesterdayRecords[0]); 896 | 897 | // 如果最早和最晚的记录不同,则都保留 898 | newDailyStats.push(firstRecord); 899 | if (lastRecord !== firstRecord) { 900 | newDailyStats.push(lastRecord); 901 | } 902 | } 903 | 904 | // 处理今天的数据:保留最早的记录和当前这个最新记录 905 | if (todayRecords.length > 0) { 906 | // 查找今天的第一条记录 907 | const firstRecord = todayRecords.reduce((earliest, current) => 908 | current.timestamp < earliest.timestamp ? current : earliest, todayRecords[0]); 909 | 910 | // 保留今天最早的记录 911 | newDailyStats.push(firstRecord); 912 | } 913 | 914 | // 添加当前最新记录 915 | const req = requirements.find(r => r.name === statName); 916 | if (req) { 917 | // 提取数字值 918 | const currentMatch = req.current.match(/(\d+)/); 919 | const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0; 920 | 921 | // 创建新记录 922 | const newRecord = { 923 | name: statName, 924 | value: currentValue, 925 | timestamp: currentTime 926 | }; 927 | 928 | // 如果今天还没有记录,或者当前值与最新记录不同,则添加 929 | const latestRecord = todayRecords.length > 0 ? 930 | todayRecords.reduce((latest, current) => 931 | current.timestamp > latest.timestamp ? current : latest, todayRecords[0]) : null; 932 | 933 | if (!latestRecord || latestRecord.value !== currentValue) { 934 | newDailyStats.push(newRecord); 935 | } 936 | } 937 | }); 938 | 939 | // 保存优化后的数据 940 | console.log('优化后的存储数据长度:', newDailyStats.length, '(之前:', dailyStats.length, ')'); 941 | console.log('存储的数据详情:', JSON.stringify(newDailyStats.map(item => ({ 942 | name: item.name, 943 | value: item.value, 944 | date: new Date(item.timestamp).toLocaleString() 945 | })))); 946 | localStorage.setItem('ld_daily_stats', JSON.stringify(newDailyStats)); 947 | 948 | return calculateDailyChanges(newDailyStats); 949 | } 950 | 951 | // 计算自然日内的活动数据 952 | function calculateDailyChanges(dailyStats) { 953 | // 定义要跟踪的数据项 954 | const statsToTrack = [ 955 | '浏览的话题(所有时间)', // 浏览话题总数 956 | '回复的话题', // 回复话题数 957 | '已读帖子(所有时间)', // 已读帖子总数 958 | '获赞:点赞用户数量', // 获赞数 959 | '点赞' // 点赞数 960 | ]; 961 | 962 | const result = {}; 963 | 964 | // 获取今天和昨天的日期范围 965 | const now = new Date(); 966 | const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); // 今天0点 967 | const yesterdayStart = todayStart - (24 * 60 * 60 * 1000); // 昨天0点 968 | const yesterdayEnd = todayStart - 1; // 昨天23:59:59 969 | 970 | console.log('时间范围: ', 971 | '今天起始:', new Date(todayStart).toLocaleString(), 972 | '昨天起始:', new Date(yesterdayStart).toLocaleString(), 973 | '昨天结束:', new Date(yesterdayEnd).toLocaleString() 974 | ); 975 | 976 | // 对于每个要跟踪的数据项,计算今天和昨天的变化 977 | statsToTrack.forEach(statName => { 978 | // 初始化结果对象结构 979 | result[statName] = { 980 | day1: 0, // 今天的变化 981 | day2: 0, // 昨天的变化 982 | trend: 0 // 趋势(今天与昨天的差异) 983 | }; 984 | 985 | // 过滤出当前数据项的所有记录 986 | const allRecords = dailyStats.filter(item => item.name === statName); 987 | if (allRecords.length === 0) { 988 | return; // 跳过这个数据项 989 | } 990 | 991 | // 查找今天和昨天自然日范围内的最早和最晚记录 992 | const todayRecords = allRecords.filter(item => item.timestamp >= todayStart); 993 | const yesterdayRecords = allRecords.filter(item => 994 | item.timestamp >= yesterdayStart && item.timestamp <= yesterdayEnd); 995 | 996 | // 找到今天的最早记录和最新记录 997 | const todayFirstRecord = todayRecords.length > 0 ? 998 | todayRecords.reduce((earliest, current) => 999 | current.timestamp < earliest.timestamp ? current : earliest, todayRecords[0]) : null; 1000 | 1001 | const todayLastRecord = todayRecords.length > 0 ? 1002 | todayRecords.reduce((latest, current) => 1003 | current.timestamp > latest.timestamp ? current : latest, todayRecords[0]) : null; 1004 | 1005 | // 找到昨天的最早记录和最晚记录 1006 | const yesterdayFirstRecord = yesterdayRecords.length > 0 ? 1007 | yesterdayRecords.reduce((earliest, current) => 1008 | current.timestamp < earliest.timestamp ? current : earliest, yesterdayRecords[0]) : null; 1009 | 1010 | const yesterdayLastRecord = yesterdayRecords.length > 0 ? 1011 | yesterdayRecords.reduce((latest, current) => 1012 | current.timestamp > latest.timestamp ? current : latest, yesterdayRecords[0]) : null; 1013 | 1014 | // 计算今天的变化值(最新记录 - 今天最早记录或昨天最晚记录) 1015 | if (todayLastRecord) { 1016 | const baseValue = todayFirstRecord ? 1017 | todayFirstRecord.value : 1018 | (yesterdayLastRecord ? yesterdayLastRecord.value : 0); 1019 | 1020 | result[statName].day1 = todayLastRecord.value - baseValue; 1021 | } 1022 | 1023 | // 计算昨天的变化值 1024 | if (yesterdayFirstRecord && yesterdayLastRecord) { 1025 | // 如果昨天有多个记录点,用最晚记录 - 最早记录 1026 | result[statName].day2 = yesterdayLastRecord.value - yesterdayFirstRecord.value; 1027 | } else if (yesterdayFirstRecord) { 1028 | // 如果昨天只有一个记录点,尝试用这个记录点减去前天的最后记录点 1029 | // 获取前天的结束时间 1030 | const dayBeforeYesterdayStart = yesterdayStart - (24 * 60 * 60 * 1000); // 前天0点 1031 | const dayBeforeYesterdayEnd = yesterdayStart - 1; // 前天23:59:59 1032 | 1033 | // 查找前天的记录 1034 | const dayBeforeYesterdayRecords = allRecords.filter(item => 1035 | item.timestamp >= dayBeforeYesterdayStart && item.timestamp <= dayBeforeYesterdayEnd); 1036 | 1037 | if (dayBeforeYesterdayRecords.length > 0) { 1038 | // 找到前天的最后一条记录 1039 | const lastDayBeforeYesterdayRecord = dayBeforeYesterdayRecords.reduce((latest, current) => 1040 | current.timestamp > latest.timestamp ? current : latest, dayBeforeYesterdayRecords[0]); 1041 | 1042 | // 计算昨天的变化:昨天唯一记录 - 前天最后记录 1043 | result[statName].day2 = yesterdayFirstRecord.value - lastDayBeforeYesterdayRecord.value; 1044 | } else { 1045 | // 如果没有前天的记录,则直接使用昨天的记录值作为变化值 1046 | // 这是一种近似处理,至少确保昨天的数据不会是0 1047 | result[statName].day2 = yesterdayFirstRecord.value; 1048 | } 1049 | } 1050 | 1051 | // 计算趋势(今天与昨天的差异) 1052 | result[statName].trend = result[statName].day1 - result[statName].day2; 1053 | 1054 | // 添加每个数据项的详细日志 1055 | console.log(`${statName} 数据计算:`, { 1056 | '今天记录数': todayRecords.length, 1057 | '昨天记录数': yesterdayRecords.length, 1058 | '今天第一条': todayFirstRecord ? 1059 | `${todayFirstRecord.value} (${new Date(todayFirstRecord.timestamp).toLocaleString()})` : '无', 1060 | '今天最后一条': todayLastRecord ? 1061 | `${todayLastRecord.value} (${new Date(todayLastRecord.timestamp).toLocaleString()})` : '无', 1062 | '昨天第一条': yesterdayFirstRecord ? 1063 | `${yesterdayFirstRecord.value} (${new Date(yesterdayFirstRecord.timestamp).toLocaleString()})` : '无', 1064 | '昨天最后一条': yesterdayLastRecord ? 1065 | `${yesterdayLastRecord.value} (${new Date(yesterdayLastRecord.timestamp).toLocaleString()})` : '无', 1066 | '今天变化': result[statName].day1, 1067 | '昨天变化': result[statName].day2, 1068 | '趋势': result[statName].trend 1069 | }); 1070 | }); 1071 | 1072 | console.log('自然日变化数据:', result); 1073 | return result; 1074 | } 1075 | 1076 | // 初始加载 1077 | fetchTrustLevelData(); 1078 | 1079 | // 恢复窗口状态和主题 1080 | // 在所有DOM操作完成后执行,确保 toggleBtn 和 themeBtn 已经定义 1081 | setTimeout(() => { 1082 | restorePanelState(); 1083 | updateThemeButtonIcon(); 1084 | }, 100); 1085 | 1086 | // 定时刷新(每五分钟) 1087 | setInterval(fetchTrustLevelData, 300000); 1088 | })(); 1089 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LDStatus 2 | 3 | LDStatus 是一个油猴脚本,用于在浏览 Linux.do 网站时显示用户的信任级别进度。通过这个脚本,您可以实时查看自己的信任级别进度,而无需频繁切换到 connect.linux.do 页面。 4 | 5 | ## 功能特点 6 | 7 | - **浮动窗口**:在 Linux.do 页面左侧显示一个可拖动的浮动窗口 8 | - **实时数据**:从 connect.linux.do 自动获取信任级别数据 9 | - **清晰展示**:以"目标: 已完成数 / 需要完成数"的形式展示数据,并显示24小时内的活动数据 10 | - **折叠功能**:支持窗口折叠为小图标,不影响浏览体验 11 | - **自动刷新**:每五分钟自动刷新数据,保持信息最新 12 | - **可拖动**:支持拖动调整窗口位置,放置在您喜欢的位置 13 | - **直观颜色**:绿色数字表示已达成目标,红色数字表示未达成目标 14 | - **主题切换**:支持深色和亮色两种主题,可根据个人喜好随时切换 15 | - **自动更新**:支持脚本自动更新,无需手动重新安装即可获取最新版本 16 | - **状态记忆**:自动记忆窗口位置和折叠状态,下次访问时自动恢复 17 | 18 | ## 安装方法 19 | 20 | ### 前提条件 21 | 22 | 在安装脚本之前,您需要先安装一个用户脚本管理器扩展。推荐使用 Tampermonkey,它支持大多数主流浏览器: 23 | 24 | - [Chrome 版 Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) 25 | - [Firefox 版 Tampermonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/) 26 | - [Edge 版 Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) 27 | - [Safari 版 Tampermonkey](https://apps.apple.com/app/apple-store/id1482490089) 28 | 29 | ### 方法一:直接安装(推荐) 30 | 31 | 1. 确保您已经安装了 Tampermonkey 或其他用户脚本管理器 32 | 2. 点击以下链接直接安装脚本: 33 | - [安装 LDStatus 脚本](https://github.com/1e0n/LinuxDoStatus/raw/master/LDStatus.user.js) 34 | 3. Tampermonkey 将自动检测并提示您安装脚本 35 | 4. 点击"安装"按钮完成安装 36 | 5. 安装后,脚本将自动检测更新,无需手动重新安装 37 | 38 | ### 方法二:手动安装 39 | 40 | 1. 安装 Tampermonkey 浏览器扩展 41 | 2. 访问 [LDStatus.user.js 文件](https://github.com/1e0n/LinuxDoStatus/blob/master/LDStatus.user.js) 42 | 3. 点击"Raw"按钮查看原始文件 43 | 4. Tampermonkey 应该会自动检测并提示安装 44 | 5. 如果没有自动提示,请手动复制文件内容 45 | 46 | ### 方法三:手动复制粘贴 47 | 48 | 1. 安装 Tampermonkey 浏览器扩展 49 | 2. 点击浏览器工具栏中的 Tampermonkey 图标 50 | 3. 选择"添加新脚本" 51 | 4. 删除编辑器中的所有默认代码 52 | 5. 将 [LDStatus.user.js](https://github.com/1e0n/LinuxDoStatus/blob/master/LDStatus.user.js) 的内容复制并粘贴到编辑器中 53 | 6. 点击"文件"菜单,然后选择"保存" 54 | 55 | ## 使用方法 56 | 57 | 安装脚本后,访问 [Linux.do](https://linux.do) 网站,脚本将自动运行并在页面左侧显示信任级别浮动窗口。 58 | 59 | - **展开/折叠**:点击窗口右上角的箭头按钮可以展开/折叠窗口 60 | - **刷新数据**:点击刷新按钮可以手动刷新数据(脚本也会每五分钟自动刷新) 61 | - **切换主题**:点击主题切换按钮(🌙/☀️)可以在深色和亮色主题之间切换 62 | - **移动窗口**:拖动窗口标题栏可以调整窗口位置 63 | - **查看进度**:绿色数字表示已达成目标,红色数字表示未达成目标 64 | - **变化标识**:当目标完成数有变化时,会显示黄色的⬆(增加)或蓝色的⬇(减少)标识及变化数值,即使刷新后数值没有变化也会保留标识 65 | - **活动统计**:在窗口底部显示24小时内的活动数据,包括浏览话题数、回复话题数、已读帖子数、获赞数和点赞数 66 | 67 | ## 注意事项 68 | 69 | - 脚本需要您已经登录 Linux.do 账号 70 | - 如果数据加载失败,请确保您已登录并刷新页面 71 | - 脚本仅在 Linux.do 域名下运行,不会在其他网站上激活 72 | 73 | ## 自动更新 74 | 75 | LDStatus 脚本支持自动更新功能。当 GitHub 仓库中的脚本版本更新后,您的浏览器将自动检测并提示您更新到最新版本。这意味着您无需手动重新安装脚本即可获得最新功能和修复。 76 | 77 | 自动更新的工作原理: 78 | 1. 脚本每天会自动检查是否有新版本 79 | 2. 如果发现新版本,Tampermonkey 会提示您更新 80 | 3. 点击更新按钮即可完成更新,无需重新访问 GitHub 81 | 82 | ## 更新日志 83 | ### v1.11 84 | - 文本优化,“获赞:单日最高数量”按照原始英文解释,这里应该指的是"获得过赞的总天数" 85 | 86 | ### v1.10 87 | - 完全重构时间统计逻辑,改用自然日计算替代相对时间窗口 88 | - "今天"一栏现在显示今天0点到现在的数据变化 89 | - "昨天"一栏现在显示昨天一整天(0-24点)的数据变化 90 | - 优化数据存储机制,每天只保留首末两个数据点,显著减少存储压力 91 | - 自动刷新间隔保持为5分钟 92 | 93 | ### v1.9 94 | - "近期的活动"区块支持显示最近两天(昨天/今天)的数据,并以列形式展示,增加趋势箭头和变化数值,便于对比每日活跃度。 95 | - 所有数字列和表头均右对齐,视觉更整齐。 96 | - 新增表头,明确每列含义(昨天/今天/变化)。 97 | - 亮色和暗色主题下,所有下部(近期的活动)文本和数值颜色与上部保持一致,风格统一。 98 | - 优化暗色主题下昨天数值的颜色,提升可读性。 99 | - 优化亮色主题下昨天和今天的数值颜色,提升对比度和一致性。 100 | - 数据自动刷新时间从两分钟改为五分钟,减少服务器压力。 101 | - 主题切换、面板拖动、折叠、刷新、自动更新等功能保持兼容。 102 | 103 | ### v1.8 104 | - 添加了亮色和深色两种主题,可以根据个人喜好切换 105 | - 在标题栏添加了主题切换按钮(🌙/☀️图标) 106 | - 修复了点赞帖子数据显示不正确的问题 107 | - 优化了数据处理逻辑,提高了数据准确性 108 | 109 | ### v1.7 110 | - 在窗口标题添加了当前脚本的版本号 111 | - 在窗口右上角添加了一个检查更新按钮(🔎图标) 112 | - 修复了之前自动更新会失败的问题 113 | 114 | ### v1.6 115 | - 添加窗口状态记忆功能,自动记忆窗口位置和折叠状态 116 | 117 | ### v1.5 118 | - 添加自动更新功能,脚本现在可以自动检测并更新到最新版本 119 | 120 | ### v1.4 121 | - 添加24小时内活动数据统计功能 122 | - 在浮动窗口底部显示用户24小时内的浏览话题数、回复话题数、已读帖子数、获赞数和点赞数 123 | 124 | ### v1.3 125 | - 修复帖子界面按钮消失的问题 126 | - 使用更特定的CSS选择器避免与网站原有元素冲突 127 | 128 | ### v1.2 129 | - 改进目标完成数变化的标识功能,即使刷新后数值没有变化也会保留标识 130 | - 增加变化标识的颜色:黄色表示增加,蓝色表示减少 131 | 132 | ### v1.1 133 | - 将数据刷新时间从每分钟改为每两分钟 134 | - 添加目标完成数变化的标识功能(⬆表示增加,⬇表示减少) 135 | 136 | ### v1.0 137 | - 初始版本发布 138 | - 实现基本的信任级别数据获取和显示 139 | - 添加浮动窗口和折叠功能 140 | - 支持自动刷新和手动刷新 141 | 142 | ## 反馈与贡献 143 | 144 | 如果您有任何问题、建议或反馈,请在 [GitHub Issues](https://github.com/1e0n/LinuxDoStatus/issues) 上提交。 145 | 146 | 欢迎通过 Pull Requests 贡献代码改进脚本。 147 | --------------------------------------------------------------------------------