标记已读帖子、显示用户详细信息(等级、主题量、鸡腿数、评论量)、自动签到 - 支持 NodeSeek 和 DeepFlood
// ==UserScript==
// @name NodeSeek & DeepFlood 增强插件
// @namespace http://tampermonkey.net/
// @version 1.5.5
// @description 标记已读帖子、显示用户详细信息(等级、主题量、鸡腿数、评论量)、自动签到 - 支持 NodeSeek 和 DeepFlood
// @author da niao
// @match https://www.nodeseek.com/*
// @match https://nodeseek.com/*
// @match https://www.deepflood.com/*
// @match https://deepflood.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// ==================== 站点识别 ====================
const CURRENT_SITE = (() => {
const host = location.hostname;
if (host.includes('nodeseek.com')) {
return {
code: 'ns',
name: 'NodeSeek',
host: host
};
} else if (host.includes('deepflood.com')) {
return {
code: 'df',
name: 'DeepFlood',
host: host
};
}
return null;
})();
// ==================== 配置管理 ====================
class ConfigManager {
static defaults = {
visitedColor: '#ff6b6b',
enableVisitedMark: true,
enableUserInfo: true,
cacheExpireTime: 24, // 小时
badgeBackground: 'transparent', // 透明背景
badgeTextColor: '#000000', // 默认黑色字体
requestDelay: 300,
autoSignIn: true,
signInMode_ns: 'random', // NodeSeek 签到模式
signInMode_df: 'random' // DeepFlood 签到模式
};
static get(key) {
const config = GM_getValue('nse_config', this.defaults);
return config[key] !== undefined ? config[key] : this.defaults[key];
}
static set(key, value) {
const config = GM_getValue('nse_config', this.defaults);
config[key] = value;
GM_setValue('nse_config', config);
}
static getAll() {
return GM_getValue('nse_config', this.defaults);
}
}
// 使用配置
const CONFIG = {
get visitedColor() { return ConfigManager.get('visitedColor'); },
get enableVisitedMark() { return ConfigManager.get('enableVisitedMark'); },
get enableUserInfo() { return ConfigManager.get('enableUserInfo'); },
get cacheExpireTime() { return ConfigManager.get('cacheExpireTime') * 3600000; }, // 转换为毫秒
get badgeBackground() { return ConfigManager.get('badgeBackground'); },
get badgeTextColor() { return ConfigManager.get('badgeTextColor'); },
get requestDelay() { return ConfigManager.get('requestDelay'); },
get autoSignIn() { return ConfigManager.get('autoSignIn'); },
get signInMode() {
const siteCode = CURRENT_SITE ? CURRENT_SITE.code : 'ns';
return ConfigManager.get(`signInMode_${siteCode}`);
}
};
// ==================== 请求队列管理 ====================
class RequestQueue {
constructor(delay = 300) {
this.queue = [];
this.processing = false;
this.delay = delay;
this.pendingRequests = new Map(); // 防止重复请求
}
async add(userId, fetchFn) {
// 如果已经有相同的请求在处理,返回该 Promise
if (this.pendingRequests.has(userId)) {
return this.pendingRequests.get(userId);
}
const promise = new Promise((resolve) => {
this.queue.push({ userId, fetchFn, resolve });
});
this.pendingRequests.set(userId, promise);
if (!this.processing) {
this.process();
}
return promise;
}
async process() {
if (this.queue.length === 0) {
this.processing = false;
return;
}
this.processing = true;
const { userId, fetchFn, resolve } = this.queue.shift();
try {
const result = await fetchFn();
resolve(result);
} catch (error) {
console.error(`请求用户 ${userId} 信息失败:`, error);
resolve(null);
} finally {
this.pendingRequests.delete(userId);
// 延迟后处理下一个请求
await new Promise(r => setTimeout(r, this.delay));
this.process();
}
}
}
const requestQueue = new RequestQueue(CONFIG.requestDelay);
// ==================== 菜单管理 ====================
class MenuManager {
static registerMenus() {
const siteCode = CURRENT_SITE ? CURRENT_SITE.code : 'ns';
const siteName = CURRENT_SITE ? CURRENT_SITE.name : 'NodeSeek';
// 签到设置(区分站点)
const signInMode = ConfigManager.get(`signInMode_${siteCode}`);
const signInText = {
'random': '🎲 随机鸡腿',
'fixed': '📌 固定5个',
'disabled': '❌ 已关闭'
}[signInMode];
GM_registerMenuCommand(`[${siteName}] 签到: ${signInText}`, () => {
const modes = ['random', 'fixed', 'disabled'];
const current = ConfigManager.get(`signInMode_${siteCode}`);
const currentIndex = modes.indexOf(current);
const nextMode = modes[(currentIndex + 1) % modes.length];
ConfigManager.set(`signInMode_${siteCode}`, nextMode);
alert(`${siteName} 签到模式已切换为: ${{'random':'随机鸡腿','fixed':'固定5个','disabled':'关闭'}[nextMode]}`);
location.reload();
});
// 已读颜色设置
GM_registerMenuCommand('🎨 设置已读颜色', () => {
const current = ConfigManager.get('visitedColor');
const color = prompt('请输入已读帖子颜色(CSS颜色值):', current);
if (color && color !== current) {
ConfigManager.set('visitedColor', color);
alert('已读颜色已更新,刷新页面生效');
location.reload();
}
});
// 徽章样式切换
const badgeStyles = {
'transparent': { name: '透明背景', bg: 'transparent', color: '#000000' },
'purple': { name: '紫色渐变', bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: '#ffffff' },
'blue': { name: '蓝色渐变', bg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', color: '#ffffff' },
'green': { name: '绿色渐变', bg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', color: '#ffffff' },
'orange': { name: '橙色渐变', bg: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', color: '#ffffff' },
'pink': { name: '粉色渐变', bg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', color: '#ffffff' },
'dark': { name: '深色背景', bg: '#2c3e50', color: '#ecf0f1' },
'light': { name: '浅色背景', bg: '#ecf0f1', color: '#2c3e50' }
};
// 获取当前样式
const currentBg = ConfigManager.get('badgeBackground');
let currentStyleName = '自定义';
for (const [key, style] of Object.entries(badgeStyles)) {
if (style.bg === currentBg) {
currentStyleName = style.name;
break;
}
}
GM_registerMenuCommand(`🎨 徽章样式: ${currentStyleName}`, () => {
const styleKeys = Object.keys(badgeStyles);
const options = styleKeys.map((key, index) =>
`${index + 1}. ${badgeStyles[key].name}`
).join('\n');
const choice = prompt(
`请选择徽章样式(输入数字):\n\n${options}\n\n当前: ${currentStyleName}`,
'1'
);
if (choice) {
const index = parseInt(choice) - 1;
if (index >= 0 && index < styleKeys.length) {
const selectedKey = styleKeys[index];
const selectedStyle = badgeStyles[selectedKey];
ConfigManager.set('badgeBackground', selectedStyle.bg);
ConfigManager.set('badgeTextColor', selectedStyle.color);
alert(`徽章样式已切换为: ${selectedStyle.name}`);
location.reload();
} else {
alert('无效的选择');
}
}
});
// 字体颜色切换
const textColors = {
'black': { name: '黑色', color: '#000000' },
'gray': { name: '深灰', color: '#333333' },
'blue': { name: '蓝色', color: '#1e90ff' },
'purple': { name: '紫色', color: '#9b59b6' },
'green': { name: '绿色', color: '#27ae60' },
'orange': { name: '橙色', color: '#e67e22' },
'red': { name: '红色', color: '#e74c3c' }
};
// 获取当前字体颜色名称
const currentTextColor = ConfigManager.get('badgeTextColor');
let currentColorName = '自定义';
for (const [key, colorObj] of Object.entries(textColors)) {
if (colorObj.color === currentTextColor) {
currentColorName = colorObj.name;
break;
}
}
GM_registerMenuCommand(`🖍️ 字体颜色: ${currentColorName}`, () => {
const colorKeys = Object.keys(textColors);
const options = colorKeys.map((key, index) =>
`${index + 1}. ${textColors[key].name}`
).join('\n');
const choice = prompt(
`请选择字体颜色(输入数字):\n\n${options}\n\n当前: ${currentColorName}`,
'1'
);
if (choice) {
const index = parseInt(choice) - 1;
if (index >= 0 && index < colorKeys.length) {
const selectedKey = colorKeys[index];
const selectedColor = textColors[selectedKey];
ConfigManager.set('badgeTextColor', selectedColor.color);
alert(`字体颜色已切换为: ${selectedColor.name}`);
location.reload();
} else {
alert('无效的选择');
}
}
});
// 缓存时间设置
const cacheHours = ConfigManager.get('cacheExpireTime');
GM_registerMenuCommand(`⏰ 缓存时间: ${cacheHours}小时`, () => {
const hours = prompt('请输入用户信息缓存时间(小时):', cacheHours);
if (hours && !isNaN(hours) && hours > 0) {
ConfigManager.set('cacheExpireTime', parseInt(hours));
alert(`缓存时间已设置为 ${hours} 小时`);
}
});
// 切换已读标记
const visitedEnabled = ConfigManager.get('enableVisitedMark');
GM_registerMenuCommand(`${visitedEnabled ? '✅' : '❌'} 已读标记`, () => {
ConfigManager.set('enableVisitedMark', !visitedEnabled);
alert(`已读标记已${!visitedEnabled ? '开启' : '关闭'}`);
location.reload();
});
// 切换用户信息显示
const userInfoEnabled = ConfigManager.get('enableUserInfo');
GM_registerMenuCommand(`${userInfoEnabled ? '✅' : '❌'} 用户信息显示`, () => {
ConfigManager.set('enableUserInfo', !userInfoEnabled);
alert(`用户信息显示已${!userInfoEnabled ? '开启' : '关闭'}`);
location.reload();
});
// 清除缓存
GM_registerMenuCommand('🗑️ 清除用户信息缓存', () => {
if (confirm('确定要清除所有用户信息缓存吗?')) {
GM_setValue('userInfoCache', {});
alert('缓存已清除');
}
});
// 重置所有设置
GM_registerMenuCommand('🔄 重置所有设置', () => {
if (confirm('确定要重置所有设置为默认值吗?')) {
GM_setValue('nse_config', ConfigManager.defaults);
GM_setValue('userInfoCache', {});
GM_setValue('visitedPosts', {});
alert('所有设置已重置,页面即将刷新');
location.reload();
}
});
}
}
// ==================== 签到管理 ====================
class SignInManager {
// 获取当前日期(北京时间)
static getCurrentDate() {
const localTimezoneOffset = (new Date()).getTimezoneOffset();
const beijingOffset = 8 * 60;
const beijingTime = new Date(Date.now() + (localTimezoneOffset + beijingOffset) * 60 * 1000);
return `${beijingTime.getFullYear()}/${(beijingTime.getMonth() + 1)}/${beijingTime.getDate()}`;
}
// 获取上次签到日期(区分站点)
static getLastSignInDate() {
const siteCode = CURRENT_SITE ? CURRENT_SITE.code : 'ns';
return GM_getValue(`lastSignInDate_${siteCode}`, '');
}
// 设置签到日期(区分站点)
static setLastSignInDate(date) {
const siteCode = CURRENT_SITE ? CURRENT_SITE.code : 'ns';
GM_setValue(`lastSignInDate_${siteCode}`, date);
}
// 检查今天是否已签到
static hasSignedInToday() {
const today = this.getCurrentDate();
const lastDate = this.getLastSignInDate();
return today === lastDate;
}
// 执行签到
static async signIn() {
if (!CURRENT_SITE) {
console.log('[增强插件] 未识别的站点');
return;
}
const signInMode = CONFIG.signInMode;
const siteName = CURRENT_SITE.name;
if (signInMode === 'disabled') {
console.log(`[${siteName}增强] 自动签到已禁用`);
return;
}
if (this.hasSignedInToday()) {
console.log(`[${siteName}增强] 今天已经签到过了`);
return;
}
const isRandom = signInMode === 'random';
try {
const response = await fetch(`/api/attendance?random=${isRandom}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
const today = this.getCurrentDate();
this.setLastSignInDate(today);
const siteName = CURRENT_SITE ? CURRENT_SITE.name : 'NodeSeek';
console.log(`[${siteName}增强] 签到成功!获得 ${data.gain} 个鸡腿,当前共有 ${data.current} 个鸡腿`);
// 可选:显示通知
this.showNotification(`${siteName} 签到成功!获得 ${data.gain} 个🍗`);
} else {
const siteName = CURRENT_SITE ? CURRENT_SITE.name : 'NodeSeek';
console.warn(`[${siteName}增强] 签到失败:`, data.message);
}
} catch (error) {
const siteName = CURRENT_SITE ? CURRENT_SITE.name : 'NodeSeek';
console.error(`[${siteName}增强] 签到请求失败:`, error);
}
}
// 显示通知(简单的页面提示)
static showNotification(message) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
font-size: 14px;
animation: slideIn 0.3s ease-out;
`;
notification.textContent = message;
// 添加动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`;
document.head.appendChild(style);
document.body.appendChild(notification);
// 3秒后自动消失
setTimeout(() => {
notification.style.animation = 'slideIn 0.3s ease-out reverse';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
}
// ==================== 存储管理 ====================
class Storage {
static getVisitedPosts() {
return GM_getValue('visitedPosts', {});
}
static markPostAsVisited(postId) {
const visited = this.getVisitedPosts();
visited[postId] = Date.now();
GM_setValue('visitedPosts', visited);
}
static isPostVisited(postId) {
const visited = this.getVisitedPosts();
return !!visited[postId];
}
static getUserInfoCache(userId) {
const cache = GM_getValue('userInfoCache', {});
const userCache = cache[userId];
if (userCache && (Date.now() - userCache.timestamp < CONFIG.cacheExpireTime)) {
return userCache.data;
}
return null;
}
static setUserInfoCache(userId, data) {
const cache = GM_getValue('userInfoCache', {});
cache[userId] = {
data: data,
timestamp: Date.now()
};
GM_setValue('userInfoCache', cache);
}
}
// ==================== 用户信息获取 ====================
class UserInfoFetcher {
// 从用户主页获取信息(使用请求队列)
static async fetchUserInfo(userId) {
// 先检查缓存
const cached = Storage.getUserInfoCache(userId);
if (cached) {
return cached;
}
// 使用请求队列,避免并发请求过多
return requestQueue.add(userId, async () => {
try {
// 再次检查缓存(可能在队列等待期间已被其他请求缓存)
const cached = Storage.getUserInfoCache(userId);
if (cached) {
return cached;
}
// 方法1: 从 API 获取
const apiData = await this.fetchFromAPI(userId);
if (apiData) {
Storage.setUserInfoCache(userId, apiData);
return apiData;
}
// 如果 API 返回 429,不再尝试备用方案,直接返回 null
return null;
} catch (error) {
console.error('获取用户信息失败:', error);
return null;
}
});
}
// 从 API 获取(使用浏览器 fetch,自动带 cookies)
static async fetchFromAPI(userId) {
try {
// 根据当前站点构建 API URL
const apiUrl = CURRENT_SITE
? `https://${CURRENT_SITE.host}/api/account/getInfo/${userId}`
: `https://www.nodeseek.com/api/account/getInfo/${userId}`;
const response = await fetch(apiUrl, {
method: 'GET',
credentials: 'include', // 自动包含 cookies
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
if (response.status === 429) {
console.warn(`API 请求过于频繁 (429),跳过用户 ${userId}`);
}
return null;
}
const data = await response.json();
// API 返回的是 detail 而不是 data
if (data.success && data.detail) {
const user = data.detail;
// 计算加入天数
let joinDays = 0;
if (user.created_at) {
const joinDate = new Date(user.created_at);
const now = new Date();
joinDays = Math.floor((now - joinDate) / (1000 * 60 * 60 * 24));
}
return {
level: user.rank || 0,
topicCount: user.nPost || 0,
drumstickCount: user.coin || 0,
commentCount: user.nComment || 0,
joinDays: joinDays
};
} else {
return null;
}
} catch (error) {
console.error('API 请求失败:', error);
return null;
}
}
}
// ==================== 收藏夹搜索 ====================
class CollectionSearch {
// 从HTML中提取当前用户ID
static getCurrentUserId() {
const userStyleLink = document.querySelector('link[href*="/userstyle/"]');
if (userStyleLink) {
const match = userStyleLink.href.match(/\/userstyle\/(\d+)\.css/);
if (match) return match[1];
}
return null;
}
// 获取所有收藏的帖子(分页获取)
static async fetchAllCollections() {
const collections = [];
let page = 1;
while (true) {
try {
const apiUrl = CURRENT_SITE
? `https://${CURRENT_SITE.host}/api/statistics/list-collection?page=${page}`
: `https://www.nodeseek.com/api/statistics/list-collection?page=${page}`;
const response = await fetch(apiUrl, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) break;
const data = await response.json();
// API 返回的是 collections 字段
if (data.success && data.collections && data.collections.length > 0) {
collections.push(...data.collections);
page++;
} else {
// 没有数据了,停止搜索
break;
}
} catch (error) {
console.error(`获取收藏夹第${page}页失败:`, error);
break;
}
}
return collections;
}
// 搜索收藏夹
static async searchCollections(keyword) {
if (!keyword || keyword.trim() === '') {
return [];
}
const collections = await this.fetchAllCollections();
const lowerKeyword = keyword.toLowerCase();
return collections.filter(item => {
const title = (item.title || '').toLowerCase();
return title.includes(lowerKeyword);
});
}
// 创建搜索框UI
static createSearchBox() {
// 查找"收藏"元素(包含"收藏 X"文本的span)
const collectionSpan = Array.from(document.querySelectorAll('span[data-v-244123cf]')).find(
span => span.textContent.includes('收藏')
);
if (!collectionSpan) {
console.log('[收藏夹搜索] 未找到收藏元素,可能不在收藏页面');
return;
}
// 找到 .user-stat 容器(黄色统计框)
const userStat = collectionSpan.closest('.user-stat');
if (!userStat) {
console.log('[收藏夹搜索] 未找到 user-stat 容器');
return;
}
// 检查是否已经添加过
if (document.querySelector('.collection-search-box')) {
return;
}
// 创建容器
const container = document.createElement('div');
container.className = 'collection-search-box';
container.style.cssText = `
display: block;
margin: 15px 0;
padding: 0;
position: relative;
width: 100%;
`;
// 创建搜索输入框
const input = document.createElement('input');
input.type = 'text';
input.placeholder = '搜索收藏...';
input.style.cssText = `
height: 36px;
padding: 0 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 100%;
outline: none;
box-sizing: border-box;
`;
// 创建结果容器
const resultsBox = document.createElement('div');
resultsBox.className = 'collection-search-results';
resultsBox.style.cssText = `
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 5px;
max-height: 400px;
overflow-y: auto;
display: none;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
// 搜索处理
let searchTimeout;
input.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const keyword = e.target.value.trim();
if (keyword === '') {
resultsBox.style.display = 'none';
return;
}
searchTimeout = setTimeout(async () => {
resultsBox.innerHTML = '<div style="padding: 10px; text-align: center; color: #999;">搜索中...</div>';
resultsBox.style.display = 'block';
const results = await this.searchCollections(keyword);
if (results.length === 0) {
resultsBox.innerHTML = '<div style="padding: 10px; text-align: center; color: #999;">未找到相关收藏</div>';
} else {
resultsBox.innerHTML = results.map(item => `
<a href="/post-${item.post_id}-1" style="
display: block;
padding: 10px;
border-bottom: 1px solid #f0f0f0;
color: #333;
text-decoration: none;
transition: background 0.2s;
" onmouseover="this.style.background='#f5f5f5'" onmouseout="this.style.background='white'">
<div style="
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
">${item.title || '无标题'}</div>
</a>
`).join('');
}
}, 300);
});
// 点击外部关闭结果框
document.addEventListener('click', (e) => {
if (!container.contains(e.target)) {
resultsBox.style.display = 'none';
}
});
container.appendChild(input);
container.appendChild(resultsBox);
// 插入到 user-stat 容器后面(黄色框外面)
userStat.insertAdjacentElement('afterend', container);
}
}
// ==================== UI 增强 ====================
class UIEnhancer {
// 创建用户信息标签
static createUserInfoBadge(userInfo) {
if (!userInfo) return null;
const badge = document.createElement('span');
badge.className = 'nodeseek-user-info';
badge.style.cssText = `
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
background: ${CONFIG.badgeBackground};
color: ${CONFIG.badgeTextColor};
border-radius: 10px;
font-size: 11px;
white-space: nowrap;
vertical-align: middle;
line-height: 1.5;
`;
// 格式化加入天数显示
let joinText = '';
if (userInfo.joinDays !== undefined) {
joinText = `<span title="加入 ${userInfo.joinDays} 天" style="margin-left: 4px;">📅${userInfo.joinDays}天</span>`;
}
badge.innerHTML = `
<span title="等级">⭐${userInfo.level}</span>
<span title="主题数" style="margin-left: 4px;">📝${userInfo.topicCount}</span>
<span title="鸡腿数" style="margin-left: 4px;">🍗${userInfo.drumstickCount}</span>
<span title="评论数" style="margin-left: 4px;">💬${userInfo.commentCount}</span>
${joinText}
`;
return badge;
}
// 标记已访问的帖子
static markVisitedPost(postElement, postId) {
if (Storage.isPostVisited(postId)) {
const titleElement = postElement.querySelector('.post-title a');
if (titleElement) {
titleElement.style.color = CONFIG.visitedColor;
}
}
}
// 为帖子列表添加用户信息(使用 IntersectionObserver 懒加载)
static enhancePostList() {
// 帖子列表项选择器
const postItems = document.querySelectorAll('.post-list-item');
// 创建 IntersectionObserver 用于懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const postItem = entry.target;
// 只处理一次
observer.unobserve(postItem);
this.enhancePostItem(postItem);
}
});
}, {
rootMargin: '100px' // 提前 100px 开始加载
});
// 观察所有帖子项
postItems.forEach(postItem => {
// 先标记已访问的帖子(不需要等待)
const postId = this.extractPostId(postItem);
if (postId && CONFIG.enableVisitedMark) {
this.markVisitedPost(postItem, postId);
}
// 使用 IntersectionObserver 懒加载用户信息
if (CONFIG.enableUserInfo) {
observer.observe(postItem);
}
});
}
// 增强单个帖子项
static async enhancePostItem(postItem) {
// 检查是否已经添加过用户信息(防止重复)
if (postItem.dataset.enhanced === 'true') {
return;
}
// 标记为已处理
postItem.dataset.enhanced = 'true';
const userLink = postItem.querySelector('a[href^="/space/"]');
if (userLink) {
const userId = this.extractUserId(userLink);
if (userId) {
const userInfo = await UserInfoFetcher.fetchUserInfo(userId);
const badge = this.createUserInfoBadge(userInfo);
if (badge) {
// 找到帖子标题的链接
const titleLink = postItem.querySelector('.post-title > a');
if (titleLink && !titleLink.nextElementSibling?.classList.contains('nodeseek-user-info')) {
titleLink.insertAdjacentElement('afterend', badge);
}
}
}
}
}
// 为帖子详情页添加用户信息(使用懒加载)
static enhancePostDetail() {
// 标记当前帖子为已访问
const postId = this.extractPostIdFromURL();
if (postId && CONFIG.enableVisitedMark) {
Storage.markPostAsVisited(postId);
}
if (!CONFIG.enableUserInfo) return;
// 获取所有评论项(包括主帖)
const contentItems = document.querySelectorAll('.content-item');
// 创建 IntersectionObserver 用于懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const contentItem = entry.target;
// 只处理一次
observer.unobserve(contentItem);
this.enhanceContentItem(contentItem);
}
});
}, {
rootMargin: '100px' // 提前 100px 开始加载
});
// 观察所有内容项
contentItems.forEach(contentItem => {
if (CONFIG.enableUserInfo) {
observer.observe(contentItem);
}
});
}
// 增强单个内容项(帖子详情页)
static async enhanceContentItem(contentItem) {
// 检查是否已经添加过用户信息
if (contentItem.dataset.enhanced === 'true') {
return;
}
// 标记为已处理
contentItem.dataset.enhanced = 'true';
// 查找用户链接(在 .author-info 或 .nsk-content-meta-info 中)
const userLink = contentItem.querySelector('.author-info a[href^="/space/"], .nsk-content-meta-info a[href^="/space/"]');
if (userLink) {
const userId = this.extractUserId(userLink);
if (userId) {
const userInfo = await UserInfoFetcher.fetchUserInfo(userId);
const badge = this.createUserInfoBadge(userInfo);
if (badge) {
// 在用户名后面插入徽章
const authorInfo = contentItem.querySelector('.author-info');
if (authorInfo && !authorInfo.querySelector('.nodeseek-user-info')) {
// 找到用户名链接后插入
const authorNameLink = authorInfo.querySelector('a[href^="/space/"]');
if (authorNameLink) {
authorNameLink.insertAdjacentElement('afterend', badge);
}
}
}
}
}
}
// 辅助方法:从元素提取帖子ID
static extractPostId(element) {
// 从帖子标题链接提取
const link = element.querySelector('a[href^="/post-"]');
if (link) {
const match = link.href.match(/\/post-(\d+)-/);
if (match) return match[1];
}
return null;
}
// 从 URL 提取帖子ID
static extractPostIdFromURL() {
const match = window.location.pathname.match(/\/post-(\d+)-/);
return match ? match[1] : null;
}
// 提取用户ID
static extractUserId(userLink) {
const match = userLink.href.match(/\/space\/(\d+)/);
return match ? match[1] : null;
}
}
// ==================== 主程序 ====================
class NodeSeekEnhancer {
static init() {
const siteName = CURRENT_SITE ? CURRENT_SITE.name : 'Unknown';
console.log(`${siteName} 增强插件已启动`);
// 根据页面类型执行不同的增强
if (this.isPostDetailPage()) {
UIEnhancer.enhancePostDetail();
} else if (this.isPostListPage()) {
UIEnhancer.enhancePostList();
}
// 监听页面变化(适用于 SPA)
this.observePageChanges();
}
static isPostDetailPage() {
return /\/post-\d+-/.test(window.location.pathname);
}
static isPostListPage() {
return window.location.pathname === '/' ||
/\/(categories|page-)/.test(window.location.pathname);
}
static observePageChanges() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
// 延迟执行,等待内容加载完成
setTimeout(() => {
if (this.isPostDetailPage()) {
UIEnhancer.enhancePostDetail();
} else if (this.isPostListPage()) {
UIEnhancer.enhancePostList();
}
}, 500);
break;
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
// ==================== 样式注入 ====================
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
.post-list .post-title a:visited {
color: ${CONFIG.visitedColor} !important;
}
`;
document.head.appendChild(style);
}
// ==================== 初始化 ====================
function initialize() {
// 注册菜单
MenuManager.registerMenus();
// 注入样式
injectStyles();
// 启动主功能
NodeSeekEnhancer.init();
// 添加收藏夹搜索框
setTimeout(() => CollectionSearch.createSearchBox(), 1000);
// 延迟执行签到
setTimeout(() => SignInManager.signIn(), 2000);
}
// 启动插件
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();