于是,我又改了一下 🤣
- 内容介绍
- 文章标签
- 相关推荐
原帖:
无聊改了一下佬的L站等级信息脚本 - 新增黑暗模式 搞七捻三原贴: 用Cursor糊了个黑暗模式和更新了一下全部达标的卡片 没有仔细读代码,可能有点臃肿,但,能用,害 没问大佬就动手改了,如果不行我再删掉吧,害 [image] // ==UserScript== // @name linux.do 等级监控浮窗 // @namespace http://tampermonkey.net/ // @version …
linux.do 等级监控浮窗
原浮窗拖动不了有时候挡住了页面,所以就让它可以拖走吧
效果:
image1920×919 112 KB
导入tampermonkey食用即可:
// ==UserScript==
// @name linux.do 等级监控浮窗
// @namespace http://tampermonkey.net/
// @version 3.1
// @description 进入 linux.do 没有登录注册按钮时,右侧显示等级浮窗,支持0-3级用户
// @author 你的名字
// @match https://linux.do/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_log
// @connect connect.linux.do
// @connect linux.do
// @connect *
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 存储数据的键名
const STORAGE_KEY = 'linux_do_user_trust_level_data_v3';
const LAST_CHECK_KEY = 'linux_do_last_check_v3';
const POSITION_KEY = 'linux_do_window_position_v3';
// 0级和1级用户的升级要求
const LEVEL_REQUIREMENTS = {
0: { // 0级升1级
topics_entered: 5,
posts_read_count: 30,
time_read: 600 // 10分钟 = 600秒
},
1: { // 1级升2级
days_visited: 15,
likes_given: 1,
likes_received: 1,
replies_to_different_topics: 3, // 特殊字段,需要单独获取
topics_entered: 20,
posts_read_count: 100,
time_read: 3600 // 60分钟 = 3600秒
}
};
// 直接在页面上添加调试浮窗
const debugDiv = document.createElement('div');
debugDiv.style.position = 'fixed';
debugDiv.style.bottom = '10px';
debugDiv.style.right = '10px';
debugDiv.style.width = '300px';
debugDiv.style.maxHeight = '200px';
debugDiv.style.overflow = 'auto';
debugDiv.style.background = 'rgba(0,0,0,0.8)';
debugDiv.style.color = '#0f0';
debugDiv.style.padding = '10px';
debugDiv.style.borderRadius = '5px';
debugDiv.style.zIndex = '10000';
debugDiv.style.fontFamily = 'monospace';
debugDiv.style.fontSize = '12px';
debugDiv.style.display = 'none'; // 默认隐藏
document.body.appendChild(debugDiv);
// 调试函数
function debugLog(message) {
const time = new Date().toLocaleTimeString();
console.log(`[Linux.do脚本] ${message}`);
GM_log(`[Linux.do脚本] ${message}`);
const logLine = document.createElement('div');
logLine.textContent = `${time}: ${message}`;
debugDiv.appendChild(logLine);
debugDiv.scrollTop = debugDiv.scrollHeight;
}
// 按Alt+D显示/隐藏调试窗口
document.addEventListener('keydown', function(e) {
if (e.altKey && e.key === 'd') {
debugDiv.style.display = debugDiv.style.display === 'none' ? 'block' : 'none';
}
});
debugLog('脚本开始执行');
// 暗黑模式检测
function isDiscourseDarkMode() {
const themeButton = document.querySelector('button[data-identifier="interface-color-selector"]');
if (themeButton) {
const useElement = themeButton.querySelector('svg use');
if (useElement) {
const href = useElement.getAttribute('href');
if (href === '#moon') {
return true; // 固定暗黑模式
}
if (href === '#sun') {
return false; // 固定亮色模式
}
if (href === '#circle-half-stroke') {
// 自动模式,根据系统偏好
const isSystemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
return isSystemDark;
}
}
}
return false; // 默认或无法检测时返回false
}
// 添加全局样式 - 全新设计
GM_addStyle(`
/* 新的悬浮按钮样式 */
:root {
--ld-bg-primary: white;
--ld-bg-secondary: #f9fafb;
--ld-bg-tertiary: #f3f4f6;
--ld-bg-disabled: #e5e7eb;
--ld-text-primary: #1f2937;
--ld-text-secondary: #374151;
--ld-text-tertiary: #4b5563;
--ld-text-muted: #6b7280;
--ld-text-disabled: #9ca3af;
--ld-border-primary: #e5e7eb;
--ld-border-secondary: #f3f4f6;
--ld-shadow-color: rgba(0, 0, 0, 0.1);
--ld-success-color: #16a34a;
--ld-success-bg: #f0fdf4;
--ld-error-color: #dc2626;
--ld-error-bg: #fef2f2;
--ld-accent-color: #ea580c;
--ld-accent-color-darker: #c2410c;
--ld-accent-bg: #fed7aa;
--ld-progress-bar-bg: linear-gradient(90deg, #fb923c, #ea580c);
}
.ld-dark-mode {
--ld-bg-primary: #2d2d2d;
--ld-bg-secondary: #252525;
--ld-bg-tertiary: #3a3a3a;
--ld-bg-disabled: #4a4a4a;
--ld-text-primary: #e0e0e0;
--ld-text-secondary: #c7c7c7;
--ld-text-tertiary: #b0b0b0;
--ld-text-muted: #8e8e8e;
--ld-text-disabled: #6e6e6e;
--ld-border-primary: #444444;
--ld-border-secondary: #383838;
--ld-shadow-color: rgba(0, 0, 0, 0.3);
--ld-success-color: #5eead4;
--ld-success-bg: #064e3b;
--ld-error-color: #fb7185;
--ld-error-bg: #4c0519;
}
.ld-floating-container {
position: fixed;
top: 50%;
right: 0;
transform: translateY(-50%);
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.ld-floating-btn {
background: var(--ld-bg-primary);
box-shadow: 0 4px 12px var(--ld-shadow-color);
border: 1px solid var(--ld-border-primary);
border-radius: 8px 0 0 8px;
border-right: none;
transition: all 0.3s ease;
cursor: move;
width: 48px;
padding: 12px 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
user-select: none;
}
.ld-floating-btn:hover {
width: 64px;
box-shadow: 0 8px 24px var(--ld-shadow-color);
}
.ld-btn-icon {
width: 16px;
height: 16px;
color: var(--ld-text-muted);
}
.ld-btn-level {
font-size: 12px;
font-weight: bold;
color: var(--ld-accent-color);
}
.ld-btn-progress-bar {
width: 32px;
height: 4px;
background: var(--ld-border-primary);
border-radius: 2px;
overflow: hidden;
}
.ld-btn-progress-fill {
height: 100%;
background: var(--ld-accent-color);
border-radius: 2px;
transition: width 0.3s ease;
}
.ld-btn-stats {
font-size: 10px;
color: var(--ld-text-muted);
}
.ld-btn-chevron {
width: 12px;
height: 12px;
color: var(--ld-text-disabled);
opacity: 0;
transition: opacity 0.3s ease;
}
.ld-floating-btn:hover .ld-btn-chevron {
opacity: 1;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 弹出窗口样式 */
.ld-popup {
position: absolute;
top: 50%;
right: 100%;
margin-right: 8px;
width: 384px;
max-height: 80vh;
background: var(--ld-bg-primary);
border-radius: 12px;
box-shadow: 0 20px 25px -5px var(--ld-shadow-color), 0 10px 10px -5px var(--ld-shadow-color);
border: 1px solid var(--ld-border-primary);
opacity: 0;
transform: translate(20px, -50%);
transition: all 0.2s ease;
pointer-events: none;
overflow: hidden;
overflow-y: auto;
}
.ld-popup.show {
opacity: 1;
transform: translate(0, -50%);
pointer-events: auto;
}
/* 当弹出窗口可能超出屏幕时的调整 */
.ld-popup.adjust-top {
top: 10px;
max-height: calc(100vh - 20px);
transform: translate(20px, 0);
}
.ld-popup.adjust-top.show {
transform: translate(0, 0);
}
.ld-popup.adjust-bottom {
top: auto;
bottom: 10px;
max-height: calc(100vh - 20px);
transform: translate(20px, 0);
}
.ld-popup.adjust-bottom.show {
transform: translate(0, 0);
}
/* Header 样式 */
.ld-popup-header {
padding: 16px;
border-bottom: 1px solid var(--ld-border-secondary);
}
.ld-header-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.ld-user-info {
display: flex;
align-items: center;
gap: 8px;
}
.ld-user-dot {
width: 12px;
height: 12px;
background: #ea580c;
border-radius: 50%;
}
.ld-user-name {
font-size: 14px;
font-weight: 500;
color: var(--ld-text-secondary);
}
.ld-level-badge {
font-size: 12px;
background: var(--ld-accent-bg);
color: var(--ld-accent-color-darker);
padding: 4px 8px;
border-radius: 9999px;
}
.ld-progress-section {
margin-top: 12px;
}
.ld-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.ld-progress-label {
font-size: 12px;
color: var(--ld-text-muted);
}
.ld-progress-stats {
font-size: 12px;
color: var(--ld-text-tertiary);
}
.ld-progress-bar-container {
width: 100%;
height: 8px;
background: var(--ld-border-primary);
border-radius: 4px;
overflow: hidden;
}
.ld-progress-bar {
height: 100%;
background: var(--ld-progress-bar-bg);
border-radius: 4px;
transition: width 0.3s ease;
}
/* 快速状态卡片 */
.ld-status-cards {
padding: 16px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.ld-status-card {
border-radius: 8px;
padding: 8px;
}
.ld-status-card.failed {
background: var(--ld-error-bg);
}
.ld-status-card.passed {
background: var(--ld-success-bg);
}
.ld-card-header {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.ld-card-icon {
width: 12px;
height: 12px;
}
.ld-card-header.failed {
color: var(--ld-error-color);
}
.ld-card-header.passed {
color: var(--ld-success-color);
}
.ld-card-title {
font-size: 12px;
font-weight: 500;
}
.ld-card-label {
font-size: 12px;
color: var(--ld-text-tertiary);
}
.ld-card-value {
font-size: 14px;
font-weight: 500;
color: var(--ld-text-primary);
}
.ld-card-subtitle {
font-size: 12px;
margin-top: 2px;
}
.ld-card-subtitle.failed {
color: var(--ld-error-color);
}
.ld-card-subtitle.passed {
color: var(--ld-success-color);
}
/* 详细列表 */
.ld-details-section {
border-top: 1px solid var(--ld-border-secondary);
}
.ld-details-list {
padding: 12px;
max-height: 256px;
overflow-y: auto;
}
.ld-detail-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s ease;
}
.ld-detail-item:hover {
background: var(--ld-bg-secondary);
}
.ld-detail-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.ld-detail-icon {
width: 12px;
height: 12px;
color: var(--ld-text-disabled);
flex-shrink: 0;
}
.ld-detail-label {
font-size: 12px;
color: var(--ld-text-tertiary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ld-detail-right {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.ld-detail-current {
font-size: 12px;
font-weight: 500;
/* color will be set dynamically */
text-align: right;
}
.ld-detail-current.passed {
color: var(--ld-success-color);
}
.ld-detail-current.failed {
color: var(--ld-error-color);
}
.ld-detail-target {
font-size: 12px;
color: var(--ld-text-disabled);
text-align: right;
}
.ld-detail-status {
width: 12px;
height: 12px;
}
.ld-detail-status.passed {
color: var(--ld-success-color);
}
.ld-detail-status.failed {
color: var(--ld-error-color);
}
/* Footer */
.ld-popup-footer {
padding: 12px;
background: var(--ld-bg-secondary);
border-top: 1px solid var(--ld-border-secondary);
text-align: center;
}
.ld-footer-message {
font-size: 12px;
font-weight: 500;
margin-bottom: 4px;
}
.ld-footer-message.failed {
color: var(--ld-error-color);
}
.ld-footer-message.passed {
color: var(--ld-success-color);
}
.ld-footer-time {
font-size: 12px;
color: var(--ld-text-muted);
}
/* 刷新按钮 */
.ld-reload-btn {
display: block;
width: calc(100% - 24px);
margin: 0 12px 12px;
padding: 8px;
background: var(--ld-bg-tertiary);
color: var(--ld-text-secondary);
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
font-size: 12px;
}
.ld-reload-btn:hover {
background: var(--ld-bg-disabled); /* Slightly darker for hover */
}
.ld-reload-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 错误状态 */
.ld-error-container {
padding: 24px;
text-align: center;
color: var(--ld-text-muted);
}
.ld-error-icon {
font-size: 24px;
color: var(--ld-error-color);
margin-bottom: 12px;
}
.ld-error-title {
font-weight: 500;
margin-bottom: 8px;
color: var(--ld-error-color);
font-size: 14px;
}
.ld-error-message {
margin-bottom: 16px;
font-size: 12px;
line-height: 1.5;
}
/* 隐藏的iframe */
.ld-hidden-iframe {
position: absolute;
width: 0;
height: 0;
border: 0;
visibility: hidden;
}
/* 响应式调整 */
@media (max-height: 600px) {
.ld-details-list {
max-height: 200px;
}
}
/* --- Left-aligned styles --- */
.ld-floating-container.ld-left-aligned .ld-floating-btn {
border-radius: 0 8px 8px 0;
border-left: none;
border-right: 1px solid var(--ld-border-primary);
}
.ld-floating-container.ld-left-aligned .ld-btn-chevron {
transform: rotate(180deg);
}
.ld-floating-container.ld-left-aligned .ld-popup {
left: 100%;
right: auto;
margin-left: 8px;
margin-right: 0;
transform: translate(-20px, -50%);
}
.ld-floating-container.ld-left-aligned .ld-popup.show {
transform: translate(0, -50%);
}
/* Adjustments for top/bottom alignment on the left side */
.ld-floating-container.ld-left-aligned .ld-popup.adjust-top {
transform: translate(-20px, 0);
}
.ld-floating-container.ld-left-aligned .ld-popup.adjust-top.show {
transform: translate(0, 0);
}
.ld-floating-container.ld-left-aligned .ld-popup.adjust-bottom {
transform: translate(-20px, 0);
}
.ld-floating-container.ld-left-aligned .ld-popup.adjust-bottom.show {
transform: translate(0, 0);
}
`);
// 工具函数:根据XPath查找元素
function getElementByXpath(xpath) {
return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
// 检查是否有注册和登录按钮
const loginBtnXpath = '//*[@id="ember3"]/div[2]/header/div/div/div[3]/span/span';
const loginBtn = getElementByXpath(loginBtnXpath);
debugLog('检查登录按钮: ' + (loginBtn ? '存在' : '不存在'));
if (loginBtn) {
// 有登录注册按钮,不执行后续逻辑
debugLog('已检测到登录按钮,不显示等级浮窗');
return;
}
// 尝试从缓存获取数据
const cachedData = GM_getValue(STORAGE_KEY);
const lastCheck = GM_getValue(LAST_CHECK_KEY, 0);
const now = Date.now();
const oneHourMs = 60 * 60 * 1000; // 一小时的毫秒数
debugLog(`上次检查时间: ${new Date(lastCheck).toLocaleString()}`);
// 创建右侧悬浮按钮容器
const container = document.createElement('div');
container.className = 'ld-floating-container';
// 加载保存的位置
const savedPosition = GM_getValue(POSITION_KEY);
if (savedPosition && savedPosition.top) {
Object.assign(container.style, {
top: savedPosition.top,
left: savedPosition.left || 'auto',
right: savedPosition.right || 'auto',
transform: 'none'
});
if (savedPosition.left === '0px') {
container.classList.add('ld-left-aligned');
}
}
// 创建悬浮按钮
const btn = document.createElement('div');
btn.className = 'ld-floating-btn';
btn.innerHTML = `
<svg class="ld-btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
<div class="ld-btn-level">L?</div>
<div class="ld-btn-progress-bar">
<div class="ld-btn-progress-fill" style="width: 0%;"></div>
</div>
<div class="ld-btn-stats">0/0</div>
<svg class="ld-btn-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
`;
// 创建浮窗
const popup = document.createElement('div');
popup.className = 'ld-popup';
// 设置默认内容
popup.innerHTML = `
<div class="ld-popup-header">
<div class="ld-header-top">
<div class="ld-user-info">
<div class="ld-user-dot"></div>
<span class="ld-user-name">加载中...</span>
</div>
<span class="ld-level-badge">升级到等级?</span>
</div>
<div class="ld-progress-section">
<div class="ld-progress-header">
<span class="ld-progress-label">完成进度</span>
<span class="ld-progress-stats">0/0</span>
</div>
<div class="ld-progress-bar-container">
<div class="ld-progress-bar" style="width: 0%;"></div>
</div>
</div>
</div>
<div class="ld-popup-content">
<div class="ld-status-cards">
<div class="ld-status-card failed">
<div class="ld-card-header failed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
<span class="ld-card-title">未达标</span>
</div>
<div class="ld-card-label">正在加载...</div>
<div class="ld-card-value">-</div>
</div>
<div class="ld-status-card passed">
<div class="ld-card-header passed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="ld-card-title">已完成</span>
</div>
<div class="ld-card-label">其他要求</div>
<div class="ld-card-value">0 / 0</div>
</div>
</div>
</div>
`;
// 添加到容器
container.appendChild(btn);
container.appendChild(popup);
// 变量用于跟踪悬停状态
let isHovered = false;
let hoverTimeout = null;
let darkModeMediaQuery = null; // 用于存储媒体查询对象
let observerDebounceTimeout = null; // 用于MutationObserver的防抖
let isDragging = false; // 用于跟踪拖动状态
// 应用暗黑模式类并设置/更新媒体查询监听器
function applyDarkModeAndSetupListeners() {
const isDark = isDiscourseDarkMode();
const wasDark = container.classList.contains('ld-dark-mode');
if (isDark !== wasDark) {
if (isDark) {
container.classList.add('ld-dark-mode');
debugLog('切换为暗黑模式');
} else {
container.classList.remove('ld-dark-mode');
debugLog('切换为亮色模式');
}
}
setupMediaQueryListener();
}
function mediaQueryChangedCallback(event) {
debugLog(`系统颜色偏好改变: ${event.matches ? '暗色' : '亮色'}`);
// 仅当Discourse主题设置为"自动"时,此回调才应触发UI更新
const themeButton = document.querySelector('button[data-identifier="interface-color-selector"]');
if (themeButton) {
const useElement = themeButton.querySelector('svg use');
if (useElement && useElement.getAttribute('href') === '#circle-half-stroke') {
applyDarkModeAndSetupListeners();
}
}
}
function setupMediaQueryListener() {
if (darkModeMediaQuery) {
darkModeMediaQuery.removeEventListener('change', mediaQueryChangedCallback);
darkModeMediaQuery = null;
}
const themeButton = document.querySelector('button[data-identifier="interface-color-selector"]');
if (themeButton) {
const useElement = themeButton.querySelector('svg use');
if (useElement && useElement.getAttribute('href') === '#circle-half-stroke') {
if (window.matchMedia) {
darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeMediaQuery.addEventListener('change', mediaQueryChangedCallback);
}
}
}
}
// 监视DOM变化以动态切换暗黑模式
const observer = new MutationObserver(mutations => {
clearTimeout(observerDebounceTimeout);
observerDebounceTimeout = setTimeout(() => {
applyDarkModeAndSetupListeners();
}, 300);
});
applyDarkModeAndSetupListeners();
// 观察body的子树变化,以捕获主题按钮图标的改变
observer.observe(document.body, {
childList: true,
subtree: true,
});
// 智能调整弹出窗口位置的函数
function adjustPopupPosition() {
const containerRect = container.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// 移除之前的调整类
popup.classList.remove('adjust-top', 'adjust-bottom');
// 强制重新计算布局
popup.offsetHeight;
// 获取弹出窗口的实际高度
const popupHeight = popup.scrollHeight;
const margin = 20; // 上下边距
// 计算弹出窗口的理想位置(居中对齐按钮)
const buttonCenterY = containerRect.top + containerRect.height / 2;
const idealTop = buttonCenterY - popupHeight / 2;
const idealBottom = idealTop + popupHeight;
debugLog(`视口高度: ${viewportHeight}, 弹窗高度: ${popupHeight}, 按钮中心Y: ${buttonCenterY}`);
debugLog(`理想顶部: ${idealTop}, 理想底部: ${idealBottom}`);
// 检查是否超出屏幕顶部
if (idealTop < margin) {
popup.classList.add('adjust-top');
debugLog('弹出窗口调整到顶部对齐');
}
// 检查是否超出屏幕底部
else if (idealBottom > viewportHeight - margin) {
popup.classList.add('adjust-bottom');
debugLog('弹出窗口调整到底部对齐');
}
// 否则使用居中对齐(默认)
else {
debugLog('弹出窗口使用居中对齐');
}
}
// 鼠标进入容器时
container.addEventListener('mouseenter', () => {
if (isDragging) return;
clearTimeout(hoverTimeout);
isHovered = true;
hoverTimeout = setTimeout(() => {
if (isHovered) {
// 调整位置
adjustPopupPosition();
// 显示弹出窗口
popup.classList.add('show');
}
}, 150); // 稍微延迟显示,避免误触
});
// 鼠标离开容器时
container.addEventListener('mouseleave', () => {
if (isDragging) return;
clearTimeout(hoverTimeout);
isHovered = false;
hoverTimeout = setTimeout(() => {
if (!isHovered) {
popup.classList.remove('show');
}
}, 100); // 稍微延迟隐藏,允许鼠标在按钮和弹窗间移动
});
// --- 拖动逻辑 ---
let dragStartX, dragStartY, dragStartTop, dragStartLeft;
function onDragMove(e) {
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
let newTop = dragStartTop + dy;
let newLeft = dragStartLeft + dx;
// 边界检查
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
if (newTop < 0) newTop = 0;
if (newLeft < 0) newLeft = 0;
if (newTop + containerHeight > viewportHeight) newTop = viewportHeight - containerHeight;
if (newLeft + containerWidth > viewportWidth) newLeft = viewportWidth - containerWidth;
container.style.top = `${newTop}px`;
container.style.left = `${newLeft}px`;
}
function onDragEnd() {
if (!isDragging) return;
isDragging = false;
btn.style.cursor = 'move';
document.body.style.userSelect = 'auto';
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
// --- 靠边吸附逻辑 ---
const viewportWidth = window.innerWidth;
const containerRect = container.getBoundingClientRect();
const containerCenter = containerRect.left + containerRect.width / 2;
let finalPosition;
if (containerCenter < viewportWidth / 2) {
// 靠左
container.style.left = '0px';
container.style.right = 'auto';
container.classList.add('ld-left-aligned');
finalPosition = { top: container.style.top, left: '0px', right: 'auto' };
} else {
// 靠右
container.style.left = 'auto';
container.style.right = '0px';
container.classList.remove('ld-left-aligned');
finalPosition = { top: container.style.top, left: 'auto', right: '0px' };
}
// 保存最终位置
GM_setValue(POSITION_KEY, finalPosition);
}
btn.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // 仅左键
isDragging = true;
e.preventDefault();
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = container.getBoundingClientRect();
container.style.right = 'auto';
container.style.transform = 'none';
container.style.top = `${rect.top}px`;
container.style.left = `${rect.left}px`;
dragStartTop = rect.top;
dragStartLeft = rect.left;
btn.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
});
// 监听窗口大小变化,重新调整位置
window.addEventListener('resize', () => {
if (popup.classList.contains('show')) {
adjustPopupPosition();
}
});
document.body.appendChild(container);
debugLog('新版按钮和浮窗已添加到页面');
// 如果有缓存数据且时间不超过一小时,直接使用缓存
if (cachedData && (now - lastCheck < oneHourMs)) {
debugLog('使用缓存数据');
updateInfo(
cachedData.username,
cachedData.currentLevel,
cachedData.targetLevel,
cachedData.trustLevelDetails,
new Date(lastCheck),
cachedData.originalHtml || '',
true // isFromCache
);
} else {
debugLog('缓存过期或不存在,准备安排获取新数据');
// 延迟后再执行,给页面一点时间稳定
const delay = 3000; // Increased delay to 3 seconds
debugLog(`将在 ${delay / 1000} 秒后尝试获取数据...`);
setTimeout(() => {
debugLog('Timeout结束,准备调用 fetchDataWithGM');
fetchDataWithGM();
}, delay);
}
// 解析信任级别详情
function parseTrustLevelDetails(targetInfoDivElement) {
const details = {
items: [],
summaryText: '',
achievedCount: 0,
totalCount: 0,
targetLevelInSummary: null // 从 "不符合信任级别 X 要求" 中提取
};
if (!targetInfoDivElement) {
debugLog('parseTrustLevelDetails: targetInfoDivElement为空');
return details;
}
// 解析表格
const table = targetInfoDivElement.querySelector('table');
if (table) {
const rows = table.querySelectorAll('tbody tr');
rows.forEach((row, index) => {
if (index === 0) return; // 跳过表头行
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
const label = cells[0].textContent.trim();
const currentText = cells[1].textContent.trim();
const requiredText = cells[2].textContent.trim();
const isMet = cells[1].classList.contains('text-green-500');
details.items.push({
label: label,
current: currentText,
required: requiredText,
isMet: isMet
});
if (isMet) {
details.achievedCount++;
}
}
});
details.totalCount = details.items.length;
} else {
debugLog('parseTrustLevelDetails: 未找到表格');
}
// 解析总结文本,例如 "不符合信任级别 3 要求,继续加油。"
const paragraphs = targetInfoDivElement.querySelectorAll('p');
paragraphs.forEach(p => {
const text = p.textContent.trim();
if (text.includes('要求') || text.includes('已满足') || text.includes('信任级别')) {
details.summaryText = text;
const levelMatch = text.match(/信任级别\s*(\d+)/);
if (levelMatch) {
details.targetLevelInSummary = levelMatch[1];
}
}
});
if (!details.summaryText) {
debugLog('parseTrustLevelDetails: 未找到总结文本段落');
}
debugLog(`parseTrustLevelDetails: 解析完成, ${details.achievedCount}/${details.totalCount} 项达标. 总结: ${details.summaryText}. 目标等级从总结文本: ${details.targetLevelInSummary}`);
return details;
}
// 使用 GM_xmlhttpRequest 获取 connect.linux.do 的信息
function fetchDataWithGM() {
debugLog('进入 fetchDataWithGM 函数,准备发起 GM_xmlhttpRequest');
try {
GM_xmlhttpRequest({
method: "GET",
url: "https://connect.linux.do/",
timeout: 15000, // 15秒超时
onload: function(response) {
debugLog(`GM_xmlhttpRequest 成功: status ${response.status}`);
if (response.status === 200) {
const responseText = response.responseText;
debugLog(`GM_xmlhttpRequest 响应状态 200,准备解析HTML。响应体长度: ${responseText.length}`);
const tempDiv = document.createElement('div');
tempDiv.innerHTML = responseText;
// 1. 解析全局用户名和当前等级 (从 <h1>)
let globalUsername = '用户';
let currentLevel = '未知';
const h1 = tempDiv.querySelector('h1');
if (h1) {
const h1Text = h1.textContent.trim();
// 例如: "你好,一剑万生 (YY_WD) 2级用户" 或 "你好, (yy2025) 0级用户"
const welcomeMatch = h1Text.match(/你好,\s*([^(\s]*)\s*\(?([^)]*)\)?\s*(\d+)级用户/i);
if (welcomeMatch) {
// 优先使用括号内的用户名,如果没有则使用前面的
globalUsername = welcomeMatch[2] || welcomeMatch[1] || '用户';
currentLevel = welcomeMatch[3];
debugLog(`从<h1>解析: 全局用户名='${globalUsername}', 当前等级='${currentLevel}'`);
} else {
debugLog(`从<h1>解析: 未匹配到欢迎信息格式: "${h1Text}"`);
}
} else {
debugLog('未在响应中找到 <h1> 标签');
}
// 检查用户等级,决定使用哪种数据获取方式
const userLevel = parseInt(currentLevel);
if (userLevel === 0 || userLevel === 1) {
debugLog(`检测到${userLevel}级用户,使用summary.json获取数据`);
fetchLowLevelUserData(globalUsername, userLevel);
} else if (userLevel >= 2) {
debugLog(`检测到${userLevel}级用户,使用connect.linux.do页面数据`);
// 继续原有逻辑处理2级及以上用户
processHighLevelUserData(tempDiv, globalUsername, currentLevel);
} else {
debugLog('无法确定用户等级,显示错误');
showError('无法确定用户等级,请检查登录状态');
}
} else {
debugLog(`请求失败,状态码: ${response.status} - ${response.statusText}`);
handleRequestError(response);
}
},
onerror: function(error) {
debugLog(`GM_xmlhttpRequest 错误: ${JSON.stringify(error)}`);
showError('网络请求错误,请检查连接和油猴插件权限');
},
ontimeout: function() {
debugLog('GM_xmlhttpRequest 超时');
showError('请求超时,请检查网络连接');
},
onabort: function() {
debugLog('GM_xmlhttpRequest 请求被中止 (onabort)');
showError('请求被中止,可能是网络问题或扩展冲突');
}
});
debugLog('GM_xmlhttpRequest 已调用,等待回调');
} catch (e) {
debugLog(`调用 GM_xmlhttpRequest 时发生同步错误: ${e.message}`);
showError('调用请求时出错,请查看日志');
}
}
// 将数据保存到缓存
function saveDataToCache(username, currentLevel, targetLevel, trustLevelDetails, originalHtml) {
debugLog('保存数据到缓存');
const dataToCache = {
username,
currentLevel,
targetLevel,
trustLevelDetails,
originalHtml,
cacheTimestamp: Date.now() // 添加一个缓存内的时间戳,方便调试
};
GM_setValue(STORAGE_KEY, dataToCache);
GM_setValue(LAST_CHECK_KEY, Date.now());
}
// 更新信息显示
function updateInfo(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache = false) {
debugLog(`更新信息: 用户='${username}', 当前L=${currentLevel}, 目标L=${targetLevel}, 详情获取=${trustLevelDetails && trustLevelDetails.items.length > 0}, 更新时间=${updateTime.toLocaleString()}`);
// 计算进度
const achievedCount = trustLevelDetails ? trustLevelDetails.achievedCount : 0;
const totalCount = trustLevelDetails ? trustLevelDetails.totalCount : 0;
const progressPercent = totalCount > 0 ? Math.round((achievedCount / totalCount) * 100) : 0;
// 更新按钮显示
const levelElement = btn.querySelector('.ld-btn-level');
const progressFill = btn.querySelector('.ld-btn-progress-fill');
const statsElement = btn.querySelector('.ld-btn-stats');
if (levelElement) levelElement.textContent = `L${currentLevel || '?'}`;
if (progressFill) progressFill.style.width = `${progressPercent}%`;
if (statsElement) statsElement.textContent = `${achievedCount}/${totalCount}`;
// 更新浮窗内容
updatePopupContent(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache);
}
// 更新浮窗内容 - 适配新UI结构
function updatePopupContent(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache = false) {
// 如果加载失败或无数据,显示错误状态
if (!trustLevelDetails || !trustLevelDetails.items || trustLevelDetails.items.length === 0) {
showPopupError('无法加载数据', '未能获取到信任级别详情数据,请刷新重试。', updateTime);
return;
}
// 计算进度
const achievedCount = trustLevelDetails.achievedCount;
const totalCount = trustLevelDetails.totalCount;
const progressPercent = Math.round((achievedCount / totalCount) * 100);
// 找到未达标的项目
const failedItems = trustLevelDetails.items.filter(item => !item.isMet);
const failedItem = failedItems.length > 0 ? failedItems[0] : null;
// 获取图标函数
function getIconSvg(type) {
const icons = {
user: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>',
message: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-3.582 8-8 8a8.991 8.991 0 01-4.92-1.487L3 21l2.513-5.08A8.991 8.991 0 013 12c0-4.418 3.582-8 8-8s8 3.582 8 8z"></path>',
eye: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>',
thumbsUp: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"></path>',
warning: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>',
shield: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>'
};
return icons[type] || icons.user;
}
function getItemIcon(label) {
if (label.includes('访问次数')) return 'user';
if (label.includes('回复') || label.includes('话题')) return 'message';
if (label.includes('浏览') || label.includes('已读')) return 'eye';
if (label.includes('举报')) return 'warning';
if (label.includes('点赞') || label.includes('获赞')) return 'thumbsUp';
if (label.includes('禁言') || label.includes('封禁')) return 'shield';
return 'user';
}
// 构建新UI HTML
let html = `
<div class="ld-popup-header">
<div class="ld-header-top">
<div class="ld-user-info">
<div class="ld-user-dot"></div>
<span class="ld-user-name">${username || '用户'}</span>
</div>
<span class="ld-level-badge">升级到等级${targetLevel}</span>
</div>
<div class="ld-progress-section">
<div class="ld-progress-header">
<span class="ld-progress-label">完成进度</span>
<span class="ld-progress-stats">${achievedCount}/${totalCount}</span>
</div>
<div class="ld-progress-bar-container">
<div class="ld-progress-bar" style="width: ${progressPercent}%;"></div>
</div>
</div>
</div>
<div class="ld-status-cards">`;
// 判断是否有失败项目
if (failedItems.length > 0) {
// 还有未达标项,显示失败卡片和成功卡片
html += `
<div class="ld-status-card failed">
<div class="ld-card-header failed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
<span class="ld-card-title">未达标</span>
</div>
<div class="ld-card-label">${failedItem ? failedItem.label : '无'}</div>
<div class="ld-card-value">${failedItem ? failedItem.current : '所有要求均已满足'}</div>
${failedItem ? `<div class="ld-card-subtitle failed">需要 ${failedItem.required}</div>` : ''}
</div>
<div class="ld-status-card passed">
<div class="ld-card-header passed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="ld-card-title">已完成</span>
</div>
<div class="ld-card-label">其他要求</div>
<div class="ld-card-value">${achievedCount} / ${totalCount}</div>
</div>`;
} else {
html += `
<div class="ld-status-card passed" style="grid-column: span 2;">
<div class="ld-card-header passed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="ld-card-title">全部达标!</span>
</div>
<div class="ld-card-value" style="font-size: 16px; margin-top: 8px;">🎉 恭喜!你已满足所有升级要求</div>
</div>`;
}
html += `
</div>
<div class="ld-details-section">
<div class="ld-details-list">`;
// 为每个指标生成HTML
trustLevelDetails.items.forEach(item => {
const iconType = getItemIcon(item.label);
html += `
<div class="ld-detail-item">
<div class="ld-detail-left">
<svg class="ld-detail-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${getIconSvg(iconType)}
</svg>
<span class="ld-detail-label">${item.label}</span>
</div>
<div class="ld-detail-right">
<span class="ld-detail-current ${item.isMet ? 'passed' : 'failed'}">${item.current}</span>
<span class="ld-detail-target">/${item.required}</span>
<svg class="ld-detail-status ${item.isMet ? 'passed' : 'failed'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${item.isMet ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>' :
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>'
}
</svg>
</div>
</div>`;
});
// 添加底部状态和更新时间
html += `
</div>
</div>
<div class="ld-popup-footer">
<div class="ld-footer-message ${failedItems.length === 0 ? 'passed' : 'failed'}">
${trustLevelDetails.summaryText || (failedItems.length === 0 ? '已满足信任级别要求' : '不符合信任级别要求,继续加油')}
</div>
<div class="ld-footer-time">更新于 ${updateTime.toLocaleString()}</div>
</div>
<button class="ld-reload-btn">刷新数据</button>`;
// 设置内容
popup.innerHTML = html;
// 添加事件监听器
setTimeout(() => {
// 刷新按钮
const reloadBtn = popup.querySelector('.ld-reload-btn');
if (reloadBtn) {
reloadBtn.addEventListener('click', function() {
this.textContent = '加载中...';
this.disabled = true;
fetchDataWithGM();
setTimeout(() => {
if (!this.isConnected) return; // 检查按钮是否还在DOM中
this.textContent = '刷新数据';
this.disabled = false;
}, 3000);
});
}
}, 100);
// 当脚本卸载时,停止观察并移除监听器
window.addEventListener('unload', () => {
if (observer) {
observer.disconnect();
debugLog('MutationObserver已停止');
}
if (darkModeMediaQuery) {
darkModeMediaQuery.removeEventListener('change', mediaQueryChangedCallback);
debugLog('已移除 prefers-color-scheme 监听器 (卸载时)');
}
clearTimeout(observerDebounceTimeout);
clearTimeout(hoverTimeout);
});
}
// 显示错误状态的浮窗
function showPopupError(title, message, updateTime) {
popup.innerHTML = `
<div class="ld-error-container">
<div class="ld-error-icon">❌</div>
<div class="ld-error-title">${title}</div>
<div class="ld-error-message">${message}</div>
<div class="ld-footer-time">尝试时间: ${updateTime ? updateTime.toLocaleString() : '未知'}</div>
</div>
<button class="ld-reload-btn">重试</button>
`;
// 添加重试按钮事件
setTimeout(() => {
const retryBtn = popup.querySelector('.ld-reload-btn');
if (retryBtn) {
retryBtn.addEventListener('click', function() {
this.textContent = '加载中...';
this.disabled = true;
fetchDataWithGM();
setTimeout(() => {
if (!this.isConnected) return;
this.textContent = '重试';
this.disabled = false;
}, 3000);
});
}
}, 100);
}
// 显示错误信息 (保留向下兼容)
function showError(message) {
debugLog(`显示错误: ${message}`);
showPopupError('出错了', message, new Date());
}
// 处理请求错误
function handleRequestError(response) {
let responseBody = response.responseText || "";
debugLog(`响应内容 (前500字符): ${responseBody.substring(0, 500)}`);
if (response.status === 429) {
showError('请求过于频繁 (429),请稍后重试。Cloudflare可能暂时限制了访问。');
} else if (responseBody.includes('Cloudflare') || responseBody.includes('challenge-platform') || responseBody.includes('Just a moment')) {
showError('Cloudflare拦截或验证页面。请等待或手动访问connect.linux.do完成验证。');
} else if (responseBody.includes('登录') || responseBody.includes('注册')) {
showError('获取数据失败,可能是需要登录 connect.linux.do。');
} else {
showError(`获取数据失败 (状态: ${response.status})`);
}
}
// 处理2级及以上用户数据(原有逻辑)
function processHighLevelUserData(tempDiv, globalUsername, currentLevel) {
let targetInfoDiv = null;
const potentialDivs = tempDiv.querySelectorAll('div.bg-white.p-6.rounded-lg.mb-4.shadow');
debugLog(`找到了 ${potentialDivs.length} 个潜在的 'div.bg-white.p-6.rounded-lg.mb-4.shadow' 元素。`);
for (let i = 0; i < potentialDivs.length; i++) {
const div = potentialDivs[i];
const h2 = div.querySelector('h2.text-xl.mb-4.font-bold');
if (h2 && h2.textContent.includes('信任级别')) {
targetInfoDiv = div;
debugLog(`找到包含"信任级别"标题的目标div,其innerHTML (前200字符): ${targetInfoDiv.innerHTML.substring(0,200)}`);
break;
}
}
if (!targetInfoDiv) {
debugLog('通过遍历和内容检查,未找到包含"信任级别"标题的目标div。');
showError('未找到包含等级信息的数据块。请检查控制台日志 (Alt+D) 中的HTML内容,并提供一个准确的选择器。');
return;
}
debugLog('通过内容匹配,在响应中找到目标信息div。');
const originalHtml = targetInfoDiv.innerHTML;
// 从目标div的<h2>解析用户名和目标等级
let specificUsername = globalUsername;
let targetLevel = '未知';
const h2InDiv = targetInfoDiv.querySelector('h2.text-xl.mb-4.font-bold');
if (h2InDiv) {
const h2Text = h2InDiv.textContent.trim();
const titleMatch = h2Text.match(/^(.+?)\s*-\s*信任级别\s*(\d+)\s*的要求/i);
if (titleMatch) {
specificUsername = titleMatch[1].trim();
targetLevel = titleMatch[2];
debugLog(`从<h2>解析: 特定用户名='${specificUsername}', 目标等级='${targetLevel}'`);
} else {
debugLog(`从<h2>解析: 未匹配到标题格式: "${h2Text}"`);
}
} else {
debugLog('目标div中未找到<h2>标签');
}
// 解析信任级别详情
const trustLevelDetails = parseTrustLevelDetails(targetInfoDiv);
debugLog(`最终提取信息: 用户名='${specificUsername}', 当前等级='${currentLevel}', 目标等级='${targetLevel}'`);
updateInfo(specificUsername, currentLevel, targetLevel, trustLevelDetails, new Date(), originalHtml);
saveDataToCache(specificUsername, currentLevel, targetLevel, trustLevelDetails, originalHtml);
}
// 处理0级和1级用户数据
function fetchLowLevelUserData(username, currentLevel) {
debugLog(`开始获取${currentLevel}级用户 ${username} 的数据`);
// 首先获取summary.json数据
GM_xmlhttpRequest({
method: "GET",
url: `https://linux.do/u/${username}/summary.json`,
timeout: 15000,
onload: function(response) {
debugLog(`summary.json请求成功: status ${response.status}`);
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
const userSummary = data.user_summary;
debugLog(`获取到用户摘要数据: ${JSON.stringify(userSummary)}`);
if (currentLevel === 1) {
// 1级用户需要额外获取回复数据
fetchUserRepliesData(username, currentLevel, userSummary);
} else {
// 0级用户直接处理数据
processLowLevelUserData(username, currentLevel, userSummary, null);
}
} catch (e) {
debugLog(`解析summary.json失败: ${e.message}`);
showError('解析用户数据失败');
}
} else {
debugLog(`summary.json请求失败: ${response.status}`);
showError(`获取用户数据失败 (状态: ${response.status})`);
}
},
onerror: function(error) {
debugLog(`summary.json请求错误: ${JSON.stringify(error)}`);
showError('获取用户数据时网络错误');
},
ontimeout: function() {
debugLog('summary.json请求超时');
showError('获取用户数据超时');
}
});
}
// 获取用户回复数据(仅1级用户需要)
function fetchUserRepliesData(username, currentLevel, userSummary) {
debugLog(`获取用户 ${username} 的回复数据`);
GM_xmlhttpRequest({
method: "GET",
url: `https://linux.do/u/${username}/activity/replies`,
timeout: 15000,
onload: function(response) {
debugLog(`replies页面请求成功: status ${response.status}`);
if (response.status === 200) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = response.responseText;
// 统计回复的不同话题数量
const replyContainer = tempDiv.querySelector('#main-outlet div:nth-child(3) section div');
let repliesCount = 0;
if (replyContainer) {
const replyItems = replyContainer.querySelectorAll('#user-content > div > div:nth-child(1) > div');
repliesCount = Math.min(replyItems.length, 3); // 最多统计3个,满足要求即可
debugLog(`找到 ${replyItems.length} 个回复项,统计 ${repliesCount} 个`);
} else {
debugLog('未找到回复容器');
}
processLowLevelUserData(username, currentLevel, userSummary, repliesCount);
} else {
debugLog(`replies页面请求失败: ${response.status}`);
// 即使获取回复数据失败,也继续处理其他数据,回复数设为0
processLowLevelUserData(username, currentLevel, userSummary, 0);
}
},
onerror: function(error) {
debugLog(`replies页面请求错误: ${JSON.stringify(error)}`);
processLowLevelUserData(username, currentLevel, userSummary, 0);
},
ontimeout: function() {
debugLog('replies页面请求超时');
processLowLevelUserData(username, currentLevel, userSummary, 0);
}
});
}
// 处理0级和1级用户的数据
function processLowLevelUserData(username, currentLevel, userSummary, repliesCount) {
debugLog(`处理${currentLevel}级用户数据: ${username}`);
const targetLevel = currentLevel + 1; // 目标等级
const requirements = LEVEL_REQUIREMENTS[currentLevel];
if (!requirements) {
showError(`未找到等级${currentLevel}的升级要求配置`);
return;
}
// 构建升级详情数据
const trustLevelDetails = {
items: [],
summaryText: '',
achievedCount: 0,
totalCount: 0,
targetLevelInSummary: targetLevel.toString()
};
// 检查各项要求
Object.entries(requirements).forEach(([key, requiredValue]) => {
let currentValue = 0;
let label = '';
let isMet = false;
switch (key) {
case 'topics_entered':
currentValue = userSummary.topics_entered || 0;
label = '浏览的话题';
isMet = currentValue >= requiredValue;
break;
case 'posts_read_count':
currentValue = userSummary.posts_read_count || 0;
label = '已读帖子';
isMet = currentValue >= requiredValue;
break;
case 'time_read':
currentValue = Math.floor((userSummary.time_read || 0) / 60); // 转换为分钟
label = '阅读时间(分钟)';
isMet = (userSummary.time_read || 0) >= requiredValue;
break;
case 'days_visited':
currentValue = userSummary.days_visited || 0;
label = '访问天数';
isMet = currentValue >= requiredValue;
break;
case 'likes_given':
currentValue = userSummary.likes_given || 0;
label = '给出的赞';
isMet = currentValue >= requiredValue;
break;
case 'likes_received':
currentValue = userSummary.likes_received || 0;
label = '收到的赞';
isMet = currentValue >= requiredValue;
break;
case 'replies_to_different_topics':
currentValue = repliesCount || 0;
label = '回复不同话题';
isMet = currentValue >= requiredValue;
break;
}
if (label) {
trustLevelDetails.items.push({
label: label,
current: currentValue.toString(),
required: key === 'time_read' ? Math.floor(requiredValue / 60).toString() : requiredValue.toString(),
isMet: isMet
});
if (isMet) {
trustLevelDetails.achievedCount++;
}
trustLevelDetails.totalCount++;
}
});
// 生成总结文本
if (trustLevelDetails.achievedCount === trustLevelDetails.totalCount) {
trustLevelDetails.summaryText = `已满足信任级别 ${targetLevel} 要求`;
} else {
trustLevelDetails.summaryText = `不符合信任级别 ${targetLevel} 要求,继续加油`;
}
debugLog(`${currentLevel}级用户数据处理完成: ${trustLevelDetails.achievedCount}/${trustLevelDetails.totalCount} 项达标`);
// 更新显示
updateInfo(username, currentLevel.toString(), targetLevel.toString(), trustLevelDetails, new Date(), '', false);
saveDataToCache(username, currentLevel.toString(), targetLevel.toString(), trustLevelDetails, '');
}
})();
网友解答:
--【壹】--:
升级了以后,这个已经不重要了
--【贰】--:
用上了,谢谢佬
--【叁】--:
感谢分享
--【肆】--:
感谢佬友
--【伍】--:
感谢大佬。
--【陆】--:
好用!谢谢佬!
--【柒】--:
感谢分享
--【捌】--:
好好好 还是3.1版本好看
--【玖】--:
谢谢大佬,直观多了
--【拾】--:
用上了,谢谢佬
--【拾壹】--:
感谢佬友分享
--【拾贰】--: 木木:
原浮窗拖动不了有时候挡住了页面,所以就让它可以拖走吧
好用
--【拾叁】--:
终于知道为啥不升级了
--【拾肆】--:
感谢大佬…
--【拾伍】--:
感谢佬友分享
--【拾陆】--:
感谢分享
--【拾柒】--:
用上啦啦啦
--【拾捌】--:
感谢佬友分享,之前那个老是挡其他部件。
--【拾玖】--:
感谢大佬。
原帖:
无聊改了一下佬的L站等级信息脚本 - 新增黑暗模式 搞七捻三原贴: 用Cursor糊了个黑暗模式和更新了一下全部达标的卡片 没有仔细读代码,可能有点臃肿,但,能用,害 没问大佬就动手改了,如果不行我再删掉吧,害 [image] // ==UserScript== // @name linux.do 等级监控浮窗 // @namespace http://tampermonkey.net/ // @version …
linux.do 等级监控浮窗
原浮窗拖动不了有时候挡住了页面,所以就让它可以拖走吧
效果:
image1920×919 112 KB
导入tampermonkey食用即可:
// ==UserScript==
// @name linux.do 等级监控浮窗
// @namespace http://tampermonkey.net/
// @version 3.1
// @description 进入 linux.do 没有登录注册按钮时,右侧显示等级浮窗,支持0-3级用户
// @author 你的名字
// @match https://linux.do/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_log
// @connect connect.linux.do
// @connect linux.do
// @connect *
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 存储数据的键名
const STORAGE_KEY = 'linux_do_user_trust_level_data_v3';
const LAST_CHECK_KEY = 'linux_do_last_check_v3';
const POSITION_KEY = 'linux_do_window_position_v3';
// 0级和1级用户的升级要求
const LEVEL_REQUIREMENTS = {
0: { // 0级升1级
topics_entered: 5,
posts_read_count: 30,
time_read: 600 // 10分钟 = 600秒
},
1: { // 1级升2级
days_visited: 15,
likes_given: 1,
likes_received: 1,
replies_to_different_topics: 3, // 特殊字段,需要单独获取
topics_entered: 20,
posts_read_count: 100,
time_read: 3600 // 60分钟 = 3600秒
}
};
// 直接在页面上添加调试浮窗
const debugDiv = document.createElement('div');
debugDiv.style.position = 'fixed';
debugDiv.style.bottom = '10px';
debugDiv.style.right = '10px';
debugDiv.style.width = '300px';
debugDiv.style.maxHeight = '200px';
debugDiv.style.overflow = 'auto';
debugDiv.style.background = 'rgba(0,0,0,0.8)';
debugDiv.style.color = '#0f0';
debugDiv.style.padding = '10px';
debugDiv.style.borderRadius = '5px';
debugDiv.style.zIndex = '10000';
debugDiv.style.fontFamily = 'monospace';
debugDiv.style.fontSize = '12px';
debugDiv.style.display = 'none'; // 默认隐藏
document.body.appendChild(debugDiv);
// 调试函数
function debugLog(message) {
const time = new Date().toLocaleTimeString();
console.log(`[Linux.do脚本] ${message}`);
GM_log(`[Linux.do脚本] ${message}`);
const logLine = document.createElement('div');
logLine.textContent = `${time}: ${message}`;
debugDiv.appendChild(logLine);
debugDiv.scrollTop = debugDiv.scrollHeight;
}
// 按Alt+D显示/隐藏调试窗口
document.addEventListener('keydown', function(e) {
if (e.altKey && e.key === 'd') {
debugDiv.style.display = debugDiv.style.display === 'none' ? 'block' : 'none';
}
});
debugLog('脚本开始执行');
// 暗黑模式检测
function isDiscourseDarkMode() {
const themeButton = document.querySelector('button[data-identifier="interface-color-selector"]');
if (themeButton) {
const useElement = themeButton.querySelector('svg use');
if (useElement) {
const href = useElement.getAttribute('href');
if (href === '#moon') {
return true; // 固定暗黑模式
}
if (href === '#sun') {
return false; // 固定亮色模式
}
if (href === '#circle-half-stroke') {
// 自动模式,根据系统偏好
const isSystemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
return isSystemDark;
}
}
}
return false; // 默认或无法检测时返回false
}
// 添加全局样式 - 全新设计
GM_addStyle(`
/* 新的悬浮按钮样式 */
:root {
--ld-bg-primary: white;
--ld-bg-secondary: #f9fafb;
--ld-bg-tertiary: #f3f4f6;
--ld-bg-disabled: #e5e7eb;
--ld-text-primary: #1f2937;
--ld-text-secondary: #374151;
--ld-text-tertiary: #4b5563;
--ld-text-muted: #6b7280;
--ld-text-disabled: #9ca3af;
--ld-border-primary: #e5e7eb;
--ld-border-secondary: #f3f4f6;
--ld-shadow-color: rgba(0, 0, 0, 0.1);
--ld-success-color: #16a34a;
--ld-success-bg: #f0fdf4;
--ld-error-color: #dc2626;
--ld-error-bg: #fef2f2;
--ld-accent-color: #ea580c;
--ld-accent-color-darker: #c2410c;
--ld-accent-bg: #fed7aa;
--ld-progress-bar-bg: linear-gradient(90deg, #fb923c, #ea580c);
}
.ld-dark-mode {
--ld-bg-primary: #2d2d2d;
--ld-bg-secondary: #252525;
--ld-bg-tertiary: #3a3a3a;
--ld-bg-disabled: #4a4a4a;
--ld-text-primary: #e0e0e0;
--ld-text-secondary: #c7c7c7;
--ld-text-tertiary: #b0b0b0;
--ld-text-muted: #8e8e8e;
--ld-text-disabled: #6e6e6e;
--ld-border-primary: #444444;
--ld-border-secondary: #383838;
--ld-shadow-color: rgba(0, 0, 0, 0.3);
--ld-success-color: #5eead4;
--ld-success-bg: #064e3b;
--ld-error-color: #fb7185;
--ld-error-bg: #4c0519;
}
.ld-floating-container {
position: fixed;
top: 50%;
right: 0;
transform: translateY(-50%);
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.ld-floating-btn {
background: var(--ld-bg-primary);
box-shadow: 0 4px 12px var(--ld-shadow-color);
border: 1px solid var(--ld-border-primary);
border-radius: 8px 0 0 8px;
border-right: none;
transition: all 0.3s ease;
cursor: move;
width: 48px;
padding: 12px 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
user-select: none;
}
.ld-floating-btn:hover {
width: 64px;
box-shadow: 0 8px 24px var(--ld-shadow-color);
}
.ld-btn-icon {
width: 16px;
height: 16px;
color: var(--ld-text-muted);
}
.ld-btn-level {
font-size: 12px;
font-weight: bold;
color: var(--ld-accent-color);
}
.ld-btn-progress-bar {
width: 32px;
height: 4px;
background: var(--ld-border-primary);
border-radius: 2px;
overflow: hidden;
}
.ld-btn-progress-fill {
height: 100%;
background: var(--ld-accent-color);
border-radius: 2px;
transition: width 0.3s ease;
}
.ld-btn-stats {
font-size: 10px;
color: var(--ld-text-muted);
}
.ld-btn-chevron {
width: 12px;
height: 12px;
color: var(--ld-text-disabled);
opacity: 0;
transition: opacity 0.3s ease;
}
.ld-floating-btn:hover .ld-btn-chevron {
opacity: 1;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 弹出窗口样式 */
.ld-popup {
position: absolute;
top: 50%;
right: 100%;
margin-right: 8px;
width: 384px;
max-height: 80vh;
background: var(--ld-bg-primary);
border-radius: 12px;
box-shadow: 0 20px 25px -5px var(--ld-shadow-color), 0 10px 10px -5px var(--ld-shadow-color);
border: 1px solid var(--ld-border-primary);
opacity: 0;
transform: translate(20px, -50%);
transition: all 0.2s ease;
pointer-events: none;
overflow: hidden;
overflow-y: auto;
}
.ld-popup.show {
opacity: 1;
transform: translate(0, -50%);
pointer-events: auto;
}
/* 当弹出窗口可能超出屏幕时的调整 */
.ld-popup.adjust-top {
top: 10px;
max-height: calc(100vh - 20px);
transform: translate(20px, 0);
}
.ld-popup.adjust-top.show {
transform: translate(0, 0);
}
.ld-popup.adjust-bottom {
top: auto;
bottom: 10px;
max-height: calc(100vh - 20px);
transform: translate(20px, 0);
}
.ld-popup.adjust-bottom.show {
transform: translate(0, 0);
}
/* Header 样式 */
.ld-popup-header {
padding: 16px;
border-bottom: 1px solid var(--ld-border-secondary);
}
.ld-header-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.ld-user-info {
display: flex;
align-items: center;
gap: 8px;
}
.ld-user-dot {
width: 12px;
height: 12px;
background: #ea580c;
border-radius: 50%;
}
.ld-user-name {
font-size: 14px;
font-weight: 500;
color: var(--ld-text-secondary);
}
.ld-level-badge {
font-size: 12px;
background: var(--ld-accent-bg);
color: var(--ld-accent-color-darker);
padding: 4px 8px;
border-radius: 9999px;
}
.ld-progress-section {
margin-top: 12px;
}
.ld-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.ld-progress-label {
font-size: 12px;
color: var(--ld-text-muted);
}
.ld-progress-stats {
font-size: 12px;
color: var(--ld-text-tertiary);
}
.ld-progress-bar-container {
width: 100%;
height: 8px;
background: var(--ld-border-primary);
border-radius: 4px;
overflow: hidden;
}
.ld-progress-bar {
height: 100%;
background: var(--ld-progress-bar-bg);
border-radius: 4px;
transition: width 0.3s ease;
}
/* 快速状态卡片 */
.ld-status-cards {
padding: 16px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.ld-status-card {
border-radius: 8px;
padding: 8px;
}
.ld-status-card.failed {
background: var(--ld-error-bg);
}
.ld-status-card.passed {
background: var(--ld-success-bg);
}
.ld-card-header {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.ld-card-icon {
width: 12px;
height: 12px;
}
.ld-card-header.failed {
color: var(--ld-error-color);
}
.ld-card-header.passed {
color: var(--ld-success-color);
}
.ld-card-title {
font-size: 12px;
font-weight: 500;
}
.ld-card-label {
font-size: 12px;
color: var(--ld-text-tertiary);
}
.ld-card-value {
font-size: 14px;
font-weight: 500;
color: var(--ld-text-primary);
}
.ld-card-subtitle {
font-size: 12px;
margin-top: 2px;
}
.ld-card-subtitle.failed {
color: var(--ld-error-color);
}
.ld-card-subtitle.passed {
color: var(--ld-success-color);
}
/* 详细列表 */
.ld-details-section {
border-top: 1px solid var(--ld-border-secondary);
}
.ld-details-list {
padding: 12px;
max-height: 256px;
overflow-y: auto;
}
.ld-detail-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s ease;
}
.ld-detail-item:hover {
background: var(--ld-bg-secondary);
}
.ld-detail-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.ld-detail-icon {
width: 12px;
height: 12px;
color: var(--ld-text-disabled);
flex-shrink: 0;
}
.ld-detail-label {
font-size: 12px;
color: var(--ld-text-tertiary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ld-detail-right {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.ld-detail-current {
font-size: 12px;
font-weight: 500;
/* color will be set dynamically */
text-align: right;
}
.ld-detail-current.passed {
color: var(--ld-success-color);
}
.ld-detail-current.failed {
color: var(--ld-error-color);
}
.ld-detail-target {
font-size: 12px;
color: var(--ld-text-disabled);
text-align: right;
}
.ld-detail-status {
width: 12px;
height: 12px;
}
.ld-detail-status.passed {
color: var(--ld-success-color);
}
.ld-detail-status.failed {
color: var(--ld-error-color);
}
/* Footer */
.ld-popup-footer {
padding: 12px;
background: var(--ld-bg-secondary);
border-top: 1px solid var(--ld-border-secondary);
text-align: center;
}
.ld-footer-message {
font-size: 12px;
font-weight: 500;
margin-bottom: 4px;
}
.ld-footer-message.failed {
color: var(--ld-error-color);
}
.ld-footer-message.passed {
color: var(--ld-success-color);
}
.ld-footer-time {
font-size: 12px;
color: var(--ld-text-muted);
}
/* 刷新按钮 */
.ld-reload-btn {
display: block;
width: calc(100% - 24px);
margin: 0 12px 12px;
padding: 8px;
background: var(--ld-bg-tertiary);
color: var(--ld-text-secondary);
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
font-size: 12px;
}
.ld-reload-btn:hover {
background: var(--ld-bg-disabled); /* Slightly darker for hover */
}
.ld-reload-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 错误状态 */
.ld-error-container {
padding: 24px;
text-align: center;
color: var(--ld-text-muted);
}
.ld-error-icon {
font-size: 24px;
color: var(--ld-error-color);
margin-bottom: 12px;
}
.ld-error-title {
font-weight: 500;
margin-bottom: 8px;
color: var(--ld-error-color);
font-size: 14px;
}
.ld-error-message {
margin-bottom: 16px;
font-size: 12px;
line-height: 1.5;
}
/* 隐藏的iframe */
.ld-hidden-iframe {
position: absolute;
width: 0;
height: 0;
border: 0;
visibility: hidden;
}
/* 响应式调整 */
@media (max-height: 600px) {
.ld-details-list {
max-height: 200px;
}
}
/* --- Left-aligned styles --- */
.ld-floating-container.ld-left-aligned .ld-floating-btn {
border-radius: 0 8px 8px 0;
border-left: none;
border-right: 1px solid var(--ld-border-primary);
}
.ld-floating-container.ld-left-aligned .ld-btn-chevron {
transform: rotate(180deg);
}
.ld-floating-container.ld-left-aligned .ld-popup {
left: 100%;
right: auto;
margin-left: 8px;
margin-right: 0;
transform: translate(-20px, -50%);
}
.ld-floating-container.ld-left-aligned .ld-popup.show {
transform: translate(0, -50%);
}
/* Adjustments for top/bottom alignment on the left side */
.ld-floating-container.ld-left-aligned .ld-popup.adjust-top {
transform: translate(-20px, 0);
}
.ld-floating-container.ld-left-aligned .ld-popup.adjust-top.show {
transform: translate(0, 0);
}
.ld-floating-container.ld-left-aligned .ld-popup.adjust-bottom {
transform: translate(-20px, 0);
}
.ld-floating-container.ld-left-aligned .ld-popup.adjust-bottom.show {
transform: translate(0, 0);
}
`);
// 工具函数:根据XPath查找元素
function getElementByXpath(xpath) {
return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
// 检查是否有注册和登录按钮
const loginBtnXpath = '//*[@id="ember3"]/div[2]/header/div/div/div[3]/span/span';
const loginBtn = getElementByXpath(loginBtnXpath);
debugLog('检查登录按钮: ' + (loginBtn ? '存在' : '不存在'));
if (loginBtn) {
// 有登录注册按钮,不执行后续逻辑
debugLog('已检测到登录按钮,不显示等级浮窗');
return;
}
// 尝试从缓存获取数据
const cachedData = GM_getValue(STORAGE_KEY);
const lastCheck = GM_getValue(LAST_CHECK_KEY, 0);
const now = Date.now();
const oneHourMs = 60 * 60 * 1000; // 一小时的毫秒数
debugLog(`上次检查时间: ${new Date(lastCheck).toLocaleString()}`);
// 创建右侧悬浮按钮容器
const container = document.createElement('div');
container.className = 'ld-floating-container';
// 加载保存的位置
const savedPosition = GM_getValue(POSITION_KEY);
if (savedPosition && savedPosition.top) {
Object.assign(container.style, {
top: savedPosition.top,
left: savedPosition.left || 'auto',
right: savedPosition.right || 'auto',
transform: 'none'
});
if (savedPosition.left === '0px') {
container.classList.add('ld-left-aligned');
}
}
// 创建悬浮按钮
const btn = document.createElement('div');
btn.className = 'ld-floating-btn';
btn.innerHTML = `
<svg class="ld-btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
<div class="ld-btn-level">L?</div>
<div class="ld-btn-progress-bar">
<div class="ld-btn-progress-fill" style="width: 0%;"></div>
</div>
<div class="ld-btn-stats">0/0</div>
<svg class="ld-btn-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
`;
// 创建浮窗
const popup = document.createElement('div');
popup.className = 'ld-popup';
// 设置默认内容
popup.innerHTML = `
<div class="ld-popup-header">
<div class="ld-header-top">
<div class="ld-user-info">
<div class="ld-user-dot"></div>
<span class="ld-user-name">加载中...</span>
</div>
<span class="ld-level-badge">升级到等级?</span>
</div>
<div class="ld-progress-section">
<div class="ld-progress-header">
<span class="ld-progress-label">完成进度</span>
<span class="ld-progress-stats">0/0</span>
</div>
<div class="ld-progress-bar-container">
<div class="ld-progress-bar" style="width: 0%;"></div>
</div>
</div>
</div>
<div class="ld-popup-content">
<div class="ld-status-cards">
<div class="ld-status-card failed">
<div class="ld-card-header failed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
<span class="ld-card-title">未达标</span>
</div>
<div class="ld-card-label">正在加载...</div>
<div class="ld-card-value">-</div>
</div>
<div class="ld-status-card passed">
<div class="ld-card-header passed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="ld-card-title">已完成</span>
</div>
<div class="ld-card-label">其他要求</div>
<div class="ld-card-value">0 / 0</div>
</div>
</div>
</div>
`;
// 添加到容器
container.appendChild(btn);
container.appendChild(popup);
// 变量用于跟踪悬停状态
let isHovered = false;
let hoverTimeout = null;
let darkModeMediaQuery = null; // 用于存储媒体查询对象
let observerDebounceTimeout = null; // 用于MutationObserver的防抖
let isDragging = false; // 用于跟踪拖动状态
// 应用暗黑模式类并设置/更新媒体查询监听器
function applyDarkModeAndSetupListeners() {
const isDark = isDiscourseDarkMode();
const wasDark = container.classList.contains('ld-dark-mode');
if (isDark !== wasDark) {
if (isDark) {
container.classList.add('ld-dark-mode');
debugLog('切换为暗黑模式');
} else {
container.classList.remove('ld-dark-mode');
debugLog('切换为亮色模式');
}
}
setupMediaQueryListener();
}
function mediaQueryChangedCallback(event) {
debugLog(`系统颜色偏好改变: ${event.matches ? '暗色' : '亮色'}`);
// 仅当Discourse主题设置为"自动"时,此回调才应触发UI更新
const themeButton = document.querySelector('button[data-identifier="interface-color-selector"]');
if (themeButton) {
const useElement = themeButton.querySelector('svg use');
if (useElement && useElement.getAttribute('href') === '#circle-half-stroke') {
applyDarkModeAndSetupListeners();
}
}
}
function setupMediaQueryListener() {
if (darkModeMediaQuery) {
darkModeMediaQuery.removeEventListener('change', mediaQueryChangedCallback);
darkModeMediaQuery = null;
}
const themeButton = document.querySelector('button[data-identifier="interface-color-selector"]');
if (themeButton) {
const useElement = themeButton.querySelector('svg use');
if (useElement && useElement.getAttribute('href') === '#circle-half-stroke') {
if (window.matchMedia) {
darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeMediaQuery.addEventListener('change', mediaQueryChangedCallback);
}
}
}
}
// 监视DOM变化以动态切换暗黑模式
const observer = new MutationObserver(mutations => {
clearTimeout(observerDebounceTimeout);
observerDebounceTimeout = setTimeout(() => {
applyDarkModeAndSetupListeners();
}, 300);
});
applyDarkModeAndSetupListeners();
// 观察body的子树变化,以捕获主题按钮图标的改变
observer.observe(document.body, {
childList: true,
subtree: true,
});
// 智能调整弹出窗口位置的函数
function adjustPopupPosition() {
const containerRect = container.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// 移除之前的调整类
popup.classList.remove('adjust-top', 'adjust-bottom');
// 强制重新计算布局
popup.offsetHeight;
// 获取弹出窗口的实际高度
const popupHeight = popup.scrollHeight;
const margin = 20; // 上下边距
// 计算弹出窗口的理想位置(居中对齐按钮)
const buttonCenterY = containerRect.top + containerRect.height / 2;
const idealTop = buttonCenterY - popupHeight / 2;
const idealBottom = idealTop + popupHeight;
debugLog(`视口高度: ${viewportHeight}, 弹窗高度: ${popupHeight}, 按钮中心Y: ${buttonCenterY}`);
debugLog(`理想顶部: ${idealTop}, 理想底部: ${idealBottom}`);
// 检查是否超出屏幕顶部
if (idealTop < margin) {
popup.classList.add('adjust-top');
debugLog('弹出窗口调整到顶部对齐');
}
// 检查是否超出屏幕底部
else if (idealBottom > viewportHeight - margin) {
popup.classList.add('adjust-bottom');
debugLog('弹出窗口调整到底部对齐');
}
// 否则使用居中对齐(默认)
else {
debugLog('弹出窗口使用居中对齐');
}
}
// 鼠标进入容器时
container.addEventListener('mouseenter', () => {
if (isDragging) return;
clearTimeout(hoverTimeout);
isHovered = true;
hoverTimeout = setTimeout(() => {
if (isHovered) {
// 调整位置
adjustPopupPosition();
// 显示弹出窗口
popup.classList.add('show');
}
}, 150); // 稍微延迟显示,避免误触
});
// 鼠标离开容器时
container.addEventListener('mouseleave', () => {
if (isDragging) return;
clearTimeout(hoverTimeout);
isHovered = false;
hoverTimeout = setTimeout(() => {
if (!isHovered) {
popup.classList.remove('show');
}
}, 100); // 稍微延迟隐藏,允许鼠标在按钮和弹窗间移动
});
// --- 拖动逻辑 ---
let dragStartX, dragStartY, dragStartTop, dragStartLeft;
function onDragMove(e) {
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
let newTop = dragStartTop + dy;
let newLeft = dragStartLeft + dx;
// 边界检查
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
if (newTop < 0) newTop = 0;
if (newLeft < 0) newLeft = 0;
if (newTop + containerHeight > viewportHeight) newTop = viewportHeight - containerHeight;
if (newLeft + containerWidth > viewportWidth) newLeft = viewportWidth - containerWidth;
container.style.top = `${newTop}px`;
container.style.left = `${newLeft}px`;
}
function onDragEnd() {
if (!isDragging) return;
isDragging = false;
btn.style.cursor = 'move';
document.body.style.userSelect = 'auto';
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
// --- 靠边吸附逻辑 ---
const viewportWidth = window.innerWidth;
const containerRect = container.getBoundingClientRect();
const containerCenter = containerRect.left + containerRect.width / 2;
let finalPosition;
if (containerCenter < viewportWidth / 2) {
// 靠左
container.style.left = '0px';
container.style.right = 'auto';
container.classList.add('ld-left-aligned');
finalPosition = { top: container.style.top, left: '0px', right: 'auto' };
} else {
// 靠右
container.style.left = 'auto';
container.style.right = '0px';
container.classList.remove('ld-left-aligned');
finalPosition = { top: container.style.top, left: 'auto', right: '0px' };
}
// 保存最终位置
GM_setValue(POSITION_KEY, finalPosition);
}
btn.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // 仅左键
isDragging = true;
e.preventDefault();
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = container.getBoundingClientRect();
container.style.right = 'auto';
container.style.transform = 'none';
container.style.top = `${rect.top}px`;
container.style.left = `${rect.left}px`;
dragStartTop = rect.top;
dragStartLeft = rect.left;
btn.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
});
// 监听窗口大小变化,重新调整位置
window.addEventListener('resize', () => {
if (popup.classList.contains('show')) {
adjustPopupPosition();
}
});
document.body.appendChild(container);
debugLog('新版按钮和浮窗已添加到页面');
// 如果有缓存数据且时间不超过一小时,直接使用缓存
if (cachedData && (now - lastCheck < oneHourMs)) {
debugLog('使用缓存数据');
updateInfo(
cachedData.username,
cachedData.currentLevel,
cachedData.targetLevel,
cachedData.trustLevelDetails,
new Date(lastCheck),
cachedData.originalHtml || '',
true // isFromCache
);
} else {
debugLog('缓存过期或不存在,准备安排获取新数据');
// 延迟后再执行,给页面一点时间稳定
const delay = 3000; // Increased delay to 3 seconds
debugLog(`将在 ${delay / 1000} 秒后尝试获取数据...`);
setTimeout(() => {
debugLog('Timeout结束,准备调用 fetchDataWithGM');
fetchDataWithGM();
}, delay);
}
// 解析信任级别详情
function parseTrustLevelDetails(targetInfoDivElement) {
const details = {
items: [],
summaryText: '',
achievedCount: 0,
totalCount: 0,
targetLevelInSummary: null // 从 "不符合信任级别 X 要求" 中提取
};
if (!targetInfoDivElement) {
debugLog('parseTrustLevelDetails: targetInfoDivElement为空');
return details;
}
// 解析表格
const table = targetInfoDivElement.querySelector('table');
if (table) {
const rows = table.querySelectorAll('tbody tr');
rows.forEach((row, index) => {
if (index === 0) return; // 跳过表头行
const cells = row.querySelectorAll('td');
if (cells.length >= 3) {
const label = cells[0].textContent.trim();
const currentText = cells[1].textContent.trim();
const requiredText = cells[2].textContent.trim();
const isMet = cells[1].classList.contains('text-green-500');
details.items.push({
label: label,
current: currentText,
required: requiredText,
isMet: isMet
});
if (isMet) {
details.achievedCount++;
}
}
});
details.totalCount = details.items.length;
} else {
debugLog('parseTrustLevelDetails: 未找到表格');
}
// 解析总结文本,例如 "不符合信任级别 3 要求,继续加油。"
const paragraphs = targetInfoDivElement.querySelectorAll('p');
paragraphs.forEach(p => {
const text = p.textContent.trim();
if (text.includes('要求') || text.includes('已满足') || text.includes('信任级别')) {
details.summaryText = text;
const levelMatch = text.match(/信任级别\s*(\d+)/);
if (levelMatch) {
details.targetLevelInSummary = levelMatch[1];
}
}
});
if (!details.summaryText) {
debugLog('parseTrustLevelDetails: 未找到总结文本段落');
}
debugLog(`parseTrustLevelDetails: 解析完成, ${details.achievedCount}/${details.totalCount} 项达标. 总结: ${details.summaryText}. 目标等级从总结文本: ${details.targetLevelInSummary}`);
return details;
}
// 使用 GM_xmlhttpRequest 获取 connect.linux.do 的信息
function fetchDataWithGM() {
debugLog('进入 fetchDataWithGM 函数,准备发起 GM_xmlhttpRequest');
try {
GM_xmlhttpRequest({
method: "GET",
url: "https://connect.linux.do/",
timeout: 15000, // 15秒超时
onload: function(response) {
debugLog(`GM_xmlhttpRequest 成功: status ${response.status}`);
if (response.status === 200) {
const responseText = response.responseText;
debugLog(`GM_xmlhttpRequest 响应状态 200,准备解析HTML。响应体长度: ${responseText.length}`);
const tempDiv = document.createElement('div');
tempDiv.innerHTML = responseText;
// 1. 解析全局用户名和当前等级 (从 <h1>)
let globalUsername = '用户';
let currentLevel = '未知';
const h1 = tempDiv.querySelector('h1');
if (h1) {
const h1Text = h1.textContent.trim();
// 例如: "你好,一剑万生 (YY_WD) 2级用户" 或 "你好, (yy2025) 0级用户"
const welcomeMatch = h1Text.match(/你好,\s*([^(\s]*)\s*\(?([^)]*)\)?\s*(\d+)级用户/i);
if (welcomeMatch) {
// 优先使用括号内的用户名,如果没有则使用前面的
globalUsername = welcomeMatch[2] || welcomeMatch[1] || '用户';
currentLevel = welcomeMatch[3];
debugLog(`从<h1>解析: 全局用户名='${globalUsername}', 当前等级='${currentLevel}'`);
} else {
debugLog(`从<h1>解析: 未匹配到欢迎信息格式: "${h1Text}"`);
}
} else {
debugLog('未在响应中找到 <h1> 标签');
}
// 检查用户等级,决定使用哪种数据获取方式
const userLevel = parseInt(currentLevel);
if (userLevel === 0 || userLevel === 1) {
debugLog(`检测到${userLevel}级用户,使用summary.json获取数据`);
fetchLowLevelUserData(globalUsername, userLevel);
} else if (userLevel >= 2) {
debugLog(`检测到${userLevel}级用户,使用connect.linux.do页面数据`);
// 继续原有逻辑处理2级及以上用户
processHighLevelUserData(tempDiv, globalUsername, currentLevel);
} else {
debugLog('无法确定用户等级,显示错误');
showError('无法确定用户等级,请检查登录状态');
}
} else {
debugLog(`请求失败,状态码: ${response.status} - ${response.statusText}`);
handleRequestError(response);
}
},
onerror: function(error) {
debugLog(`GM_xmlhttpRequest 错误: ${JSON.stringify(error)}`);
showError('网络请求错误,请检查连接和油猴插件权限');
},
ontimeout: function() {
debugLog('GM_xmlhttpRequest 超时');
showError('请求超时,请检查网络连接');
},
onabort: function() {
debugLog('GM_xmlhttpRequest 请求被中止 (onabort)');
showError('请求被中止,可能是网络问题或扩展冲突');
}
});
debugLog('GM_xmlhttpRequest 已调用,等待回调');
} catch (e) {
debugLog(`调用 GM_xmlhttpRequest 时发生同步错误: ${e.message}`);
showError('调用请求时出错,请查看日志');
}
}
// 将数据保存到缓存
function saveDataToCache(username, currentLevel, targetLevel, trustLevelDetails, originalHtml) {
debugLog('保存数据到缓存');
const dataToCache = {
username,
currentLevel,
targetLevel,
trustLevelDetails,
originalHtml,
cacheTimestamp: Date.now() // 添加一个缓存内的时间戳,方便调试
};
GM_setValue(STORAGE_KEY, dataToCache);
GM_setValue(LAST_CHECK_KEY, Date.now());
}
// 更新信息显示
function updateInfo(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache = false) {
debugLog(`更新信息: 用户='${username}', 当前L=${currentLevel}, 目标L=${targetLevel}, 详情获取=${trustLevelDetails && trustLevelDetails.items.length > 0}, 更新时间=${updateTime.toLocaleString()}`);
// 计算进度
const achievedCount = trustLevelDetails ? trustLevelDetails.achievedCount : 0;
const totalCount = trustLevelDetails ? trustLevelDetails.totalCount : 0;
const progressPercent = totalCount > 0 ? Math.round((achievedCount / totalCount) * 100) : 0;
// 更新按钮显示
const levelElement = btn.querySelector('.ld-btn-level');
const progressFill = btn.querySelector('.ld-btn-progress-fill');
const statsElement = btn.querySelector('.ld-btn-stats');
if (levelElement) levelElement.textContent = `L${currentLevel || '?'}`;
if (progressFill) progressFill.style.width = `${progressPercent}%`;
if (statsElement) statsElement.textContent = `${achievedCount}/${totalCount}`;
// 更新浮窗内容
updatePopupContent(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache);
}
// 更新浮窗内容 - 适配新UI结构
function updatePopupContent(username, currentLevel, targetLevel, trustLevelDetails, updateTime, originalHtml, isFromCache = false) {
// 如果加载失败或无数据,显示错误状态
if (!trustLevelDetails || !trustLevelDetails.items || trustLevelDetails.items.length === 0) {
showPopupError('无法加载数据', '未能获取到信任级别详情数据,请刷新重试。', updateTime);
return;
}
// 计算进度
const achievedCount = trustLevelDetails.achievedCount;
const totalCount = trustLevelDetails.totalCount;
const progressPercent = Math.round((achievedCount / totalCount) * 100);
// 找到未达标的项目
const failedItems = trustLevelDetails.items.filter(item => !item.isMet);
const failedItem = failedItems.length > 0 ? failedItems[0] : null;
// 获取图标函数
function getIconSvg(type) {
const icons = {
user: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>',
message: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-3.582 8-8 8a8.991 8.991 0 01-4.92-1.487L3 21l2.513-5.08A8.991 8.991 0 013 12c0-4.418 3.582-8 8-8s8 3.582 8 8z"></path>',
eye: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>',
thumbsUp: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"></path>',
warning: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>',
shield: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>'
};
return icons[type] || icons.user;
}
function getItemIcon(label) {
if (label.includes('访问次数')) return 'user';
if (label.includes('回复') || label.includes('话题')) return 'message';
if (label.includes('浏览') || label.includes('已读')) return 'eye';
if (label.includes('举报')) return 'warning';
if (label.includes('点赞') || label.includes('获赞')) return 'thumbsUp';
if (label.includes('禁言') || label.includes('封禁')) return 'shield';
return 'user';
}
// 构建新UI HTML
let html = `
<div class="ld-popup-header">
<div class="ld-header-top">
<div class="ld-user-info">
<div class="ld-user-dot"></div>
<span class="ld-user-name">${username || '用户'}</span>
</div>
<span class="ld-level-badge">升级到等级${targetLevel}</span>
</div>
<div class="ld-progress-section">
<div class="ld-progress-header">
<span class="ld-progress-label">完成进度</span>
<span class="ld-progress-stats">${achievedCount}/${totalCount}</span>
</div>
<div class="ld-progress-bar-container">
<div class="ld-progress-bar" style="width: ${progressPercent}%;"></div>
</div>
</div>
</div>
<div class="ld-status-cards">`;
// 判断是否有失败项目
if (failedItems.length > 0) {
// 还有未达标项,显示失败卡片和成功卡片
html += `
<div class="ld-status-card failed">
<div class="ld-card-header failed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
<span class="ld-card-title">未达标</span>
</div>
<div class="ld-card-label">${failedItem ? failedItem.label : '无'}</div>
<div class="ld-card-value">${failedItem ? failedItem.current : '所有要求均已满足'}</div>
${failedItem ? `<div class="ld-card-subtitle failed">需要 ${failedItem.required}</div>` : ''}
</div>
<div class="ld-status-card passed">
<div class="ld-card-header passed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="ld-card-title">已完成</span>
</div>
<div class="ld-card-label">其他要求</div>
<div class="ld-card-value">${achievedCount} / ${totalCount}</div>
</div>`;
} else {
html += `
<div class="ld-status-card passed" style="grid-column: span 2;">
<div class="ld-card-header passed">
<svg class="ld-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="ld-card-title">全部达标!</span>
</div>
<div class="ld-card-value" style="font-size: 16px; margin-top: 8px;">🎉 恭喜!你已满足所有升级要求</div>
</div>`;
}
html += `
</div>
<div class="ld-details-section">
<div class="ld-details-list">`;
// 为每个指标生成HTML
trustLevelDetails.items.forEach(item => {
const iconType = getItemIcon(item.label);
html += `
<div class="ld-detail-item">
<div class="ld-detail-left">
<svg class="ld-detail-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${getIconSvg(iconType)}
</svg>
<span class="ld-detail-label">${item.label}</span>
</div>
<div class="ld-detail-right">
<span class="ld-detail-current ${item.isMet ? 'passed' : 'failed'}">${item.current}</span>
<span class="ld-detail-target">/${item.required}</span>
<svg class="ld-detail-status ${item.isMet ? 'passed' : 'failed'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${item.isMet ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>' :
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>'
}
</svg>
</div>
</div>`;
});
// 添加底部状态和更新时间
html += `
</div>
</div>
<div class="ld-popup-footer">
<div class="ld-footer-message ${failedItems.length === 0 ? 'passed' : 'failed'}">
${trustLevelDetails.summaryText || (failedItems.length === 0 ? '已满足信任级别要求' : '不符合信任级别要求,继续加油')}
</div>
<div class="ld-footer-time">更新于 ${updateTime.toLocaleString()}</div>
</div>
<button class="ld-reload-btn">刷新数据</button>`;
// 设置内容
popup.innerHTML = html;
// 添加事件监听器
setTimeout(() => {
// 刷新按钮
const reloadBtn = popup.querySelector('.ld-reload-btn');
if (reloadBtn) {
reloadBtn.addEventListener('click', function() {
this.textContent = '加载中...';
this.disabled = true;
fetchDataWithGM();
setTimeout(() => {
if (!this.isConnected) return; // 检查按钮是否还在DOM中
this.textContent = '刷新数据';
this.disabled = false;
}, 3000);
});
}
}, 100);
// 当脚本卸载时,停止观察并移除监听器
window.addEventListener('unload', () => {
if (observer) {
observer.disconnect();
debugLog('MutationObserver已停止');
}
if (darkModeMediaQuery) {
darkModeMediaQuery.removeEventListener('change', mediaQueryChangedCallback);
debugLog('已移除 prefers-color-scheme 监听器 (卸载时)');
}
clearTimeout(observerDebounceTimeout);
clearTimeout(hoverTimeout);
});
}
// 显示错误状态的浮窗
function showPopupError(title, message, updateTime) {
popup.innerHTML = `
<div class="ld-error-container">
<div class="ld-error-icon">❌</div>
<div class="ld-error-title">${title}</div>
<div class="ld-error-message">${message}</div>
<div class="ld-footer-time">尝试时间: ${updateTime ? updateTime.toLocaleString() : '未知'}</div>
</div>
<button class="ld-reload-btn">重试</button>
`;
// 添加重试按钮事件
setTimeout(() => {
const retryBtn = popup.querySelector('.ld-reload-btn');
if (retryBtn) {
retryBtn.addEventListener('click', function() {
this.textContent = '加载中...';
this.disabled = true;
fetchDataWithGM();
setTimeout(() => {
if (!this.isConnected) return;
this.textContent = '重试';
this.disabled = false;
}, 3000);
});
}
}, 100);
}
// 显示错误信息 (保留向下兼容)
function showError(message) {
debugLog(`显示错误: ${message}`);
showPopupError('出错了', message, new Date());
}
// 处理请求错误
function handleRequestError(response) {
let responseBody = response.responseText || "";
debugLog(`响应内容 (前500字符): ${responseBody.substring(0, 500)}`);
if (response.status === 429) {
showError('请求过于频繁 (429),请稍后重试。Cloudflare可能暂时限制了访问。');
} else if (responseBody.includes('Cloudflare') || responseBody.includes('challenge-platform') || responseBody.includes('Just a moment')) {
showError('Cloudflare拦截或验证页面。请等待或手动访问connect.linux.do完成验证。');
} else if (responseBody.includes('登录') || responseBody.includes('注册')) {
showError('获取数据失败,可能是需要登录 connect.linux.do。');
} else {
showError(`获取数据失败 (状态: ${response.status})`);
}
}
// 处理2级及以上用户数据(原有逻辑)
function processHighLevelUserData(tempDiv, globalUsername, currentLevel) {
let targetInfoDiv = null;
const potentialDivs = tempDiv.querySelectorAll('div.bg-white.p-6.rounded-lg.mb-4.shadow');
debugLog(`找到了 ${potentialDivs.length} 个潜在的 'div.bg-white.p-6.rounded-lg.mb-4.shadow' 元素。`);
for (let i = 0; i < potentialDivs.length; i++) {
const div = potentialDivs[i];
const h2 = div.querySelector('h2.text-xl.mb-4.font-bold');
if (h2 && h2.textContent.includes('信任级别')) {
targetInfoDiv = div;
debugLog(`找到包含"信任级别"标题的目标div,其innerHTML (前200字符): ${targetInfoDiv.innerHTML.substring(0,200)}`);
break;
}
}
if (!targetInfoDiv) {
debugLog('通过遍历和内容检查,未找到包含"信任级别"标题的目标div。');
showError('未找到包含等级信息的数据块。请检查控制台日志 (Alt+D) 中的HTML内容,并提供一个准确的选择器。');
return;
}
debugLog('通过内容匹配,在响应中找到目标信息div。');
const originalHtml = targetInfoDiv.innerHTML;
// 从目标div的<h2>解析用户名和目标等级
let specificUsername = globalUsername;
let targetLevel = '未知';
const h2InDiv = targetInfoDiv.querySelector('h2.text-xl.mb-4.font-bold');
if (h2InDiv) {
const h2Text = h2InDiv.textContent.trim();
const titleMatch = h2Text.match(/^(.+?)\s*-\s*信任级别\s*(\d+)\s*的要求/i);
if (titleMatch) {
specificUsername = titleMatch[1].trim();
targetLevel = titleMatch[2];
debugLog(`从<h2>解析: 特定用户名='${specificUsername}', 目标等级='${targetLevel}'`);
} else {
debugLog(`从<h2>解析: 未匹配到标题格式: "${h2Text}"`);
}
} else {
debugLog('目标div中未找到<h2>标签');
}
// 解析信任级别详情
const trustLevelDetails = parseTrustLevelDetails(targetInfoDiv);
debugLog(`最终提取信息: 用户名='${specificUsername}', 当前等级='${currentLevel}', 目标等级='${targetLevel}'`);
updateInfo(specificUsername, currentLevel, targetLevel, trustLevelDetails, new Date(), originalHtml);
saveDataToCache(specificUsername, currentLevel, targetLevel, trustLevelDetails, originalHtml);
}
// 处理0级和1级用户数据
function fetchLowLevelUserData(username, currentLevel) {
debugLog(`开始获取${currentLevel}级用户 ${username} 的数据`);
// 首先获取summary.json数据
GM_xmlhttpRequest({
method: "GET",
url: `https://linux.do/u/${username}/summary.json`,
timeout: 15000,
onload: function(response) {
debugLog(`summary.json请求成功: status ${response.status}`);
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
const userSummary = data.user_summary;
debugLog(`获取到用户摘要数据: ${JSON.stringify(userSummary)}`);
if (currentLevel === 1) {
// 1级用户需要额外获取回复数据
fetchUserRepliesData(username, currentLevel, userSummary);
} else {
// 0级用户直接处理数据
processLowLevelUserData(username, currentLevel, userSummary, null);
}
} catch (e) {
debugLog(`解析summary.json失败: ${e.message}`);
showError('解析用户数据失败');
}
} else {
debugLog(`summary.json请求失败: ${response.status}`);
showError(`获取用户数据失败 (状态: ${response.status})`);
}
},
onerror: function(error) {
debugLog(`summary.json请求错误: ${JSON.stringify(error)}`);
showError('获取用户数据时网络错误');
},
ontimeout: function() {
debugLog('summary.json请求超时');
showError('获取用户数据超时');
}
});
}
// 获取用户回复数据(仅1级用户需要)
function fetchUserRepliesData(username, currentLevel, userSummary) {
debugLog(`获取用户 ${username} 的回复数据`);
GM_xmlhttpRequest({
method: "GET",
url: `https://linux.do/u/${username}/activity/replies`,
timeout: 15000,
onload: function(response) {
debugLog(`replies页面请求成功: status ${response.status}`);
if (response.status === 200) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = response.responseText;
// 统计回复的不同话题数量
const replyContainer = tempDiv.querySelector('#main-outlet div:nth-child(3) section div');
let repliesCount = 0;
if (replyContainer) {
const replyItems = replyContainer.querySelectorAll('#user-content > div > div:nth-child(1) > div');
repliesCount = Math.min(replyItems.length, 3); // 最多统计3个,满足要求即可
debugLog(`找到 ${replyItems.length} 个回复项,统计 ${repliesCount} 个`);
} else {
debugLog('未找到回复容器');
}
processLowLevelUserData(username, currentLevel, userSummary, repliesCount);
} else {
debugLog(`replies页面请求失败: ${response.status}`);
// 即使获取回复数据失败,也继续处理其他数据,回复数设为0
processLowLevelUserData(username, currentLevel, userSummary, 0);
}
},
onerror: function(error) {
debugLog(`replies页面请求错误: ${JSON.stringify(error)}`);
processLowLevelUserData(username, currentLevel, userSummary, 0);
},
ontimeout: function() {
debugLog('replies页面请求超时');
processLowLevelUserData(username, currentLevel, userSummary, 0);
}
});
}
// 处理0级和1级用户的数据
function processLowLevelUserData(username, currentLevel, userSummary, repliesCount) {
debugLog(`处理${currentLevel}级用户数据: ${username}`);
const targetLevel = currentLevel + 1; // 目标等级
const requirements = LEVEL_REQUIREMENTS[currentLevel];
if (!requirements) {
showError(`未找到等级${currentLevel}的升级要求配置`);
return;
}
// 构建升级详情数据
const trustLevelDetails = {
items: [],
summaryText: '',
achievedCount: 0,
totalCount: 0,
targetLevelInSummary: targetLevel.toString()
};
// 检查各项要求
Object.entries(requirements).forEach(([key, requiredValue]) => {
let currentValue = 0;
let label = '';
let isMet = false;
switch (key) {
case 'topics_entered':
currentValue = userSummary.topics_entered || 0;
label = '浏览的话题';
isMet = currentValue >= requiredValue;
break;
case 'posts_read_count':
currentValue = userSummary.posts_read_count || 0;
label = '已读帖子';
isMet = currentValue >= requiredValue;
break;
case 'time_read':
currentValue = Math.floor((userSummary.time_read || 0) / 60); // 转换为分钟
label = '阅读时间(分钟)';
isMet = (userSummary.time_read || 0) >= requiredValue;
break;
case 'days_visited':
currentValue = userSummary.days_visited || 0;
label = '访问天数';
isMet = currentValue >= requiredValue;
break;
case 'likes_given':
currentValue = userSummary.likes_given || 0;
label = '给出的赞';
isMet = currentValue >= requiredValue;
break;
case 'likes_received':
currentValue = userSummary.likes_received || 0;
label = '收到的赞';
isMet = currentValue >= requiredValue;
break;
case 'replies_to_different_topics':
currentValue = repliesCount || 0;
label = '回复不同话题';
isMet = currentValue >= requiredValue;
break;
}
if (label) {
trustLevelDetails.items.push({
label: label,
current: currentValue.toString(),
required: key === 'time_read' ? Math.floor(requiredValue / 60).toString() : requiredValue.toString(),
isMet: isMet
});
if (isMet) {
trustLevelDetails.achievedCount++;
}
trustLevelDetails.totalCount++;
}
});
// 生成总结文本
if (trustLevelDetails.achievedCount === trustLevelDetails.totalCount) {
trustLevelDetails.summaryText = `已满足信任级别 ${targetLevel} 要求`;
} else {
trustLevelDetails.summaryText = `不符合信任级别 ${targetLevel} 要求,继续加油`;
}
debugLog(`${currentLevel}级用户数据处理完成: ${trustLevelDetails.achievedCount}/${trustLevelDetails.totalCount} 项达标`);
// 更新显示
updateInfo(username, currentLevel.toString(), targetLevel.toString(), trustLevelDetails, new Date(), '', false);
saveDataToCache(username, currentLevel.toString(), targetLevel.toString(), trustLevelDetails, '');
}
})();
网友解答:
--【壹】--:
升级了以后,这个已经不重要了
--【贰】--:
用上了,谢谢佬
--【叁】--:
感谢分享
--【肆】--:
感谢佬友
--【伍】--:
感谢大佬。
--【陆】--:
好用!谢谢佬!
--【柒】--:
感谢分享
--【捌】--:
好好好 还是3.1版本好看
--【玖】--:
谢谢大佬,直观多了
--【拾】--:
用上了,谢谢佬
--【拾壹】--:
感谢佬友分享
--【拾贰】--: 木木:
原浮窗拖动不了有时候挡住了页面,所以就让它可以拖走吧
好用
--【拾叁】--:
终于知道为啥不升级了
--【拾肆】--:
感谢大佬…
--【拾伍】--:
感谢佬友分享
--【拾陆】--:
感谢分享
--【拾柒】--:
用上啦啦啦
--【拾捌】--:
感谢佬友分享,之前那个老是挡其他部件。
--【拾玖】--:
感谢大佬。

