// ==UserScript== // @name 龙空信息降噪器 v0.4.1 // @namespace http://tampermonkey.net/ // @version 0.4.1 // @description 让您的龙空浏览体验回归平静与高效。新增显示用户名功能。安装任何插件,在浏览器运行任何代码前,请先问问AI,防止代码中包含恶意攻击内容。 // @author liudev & AI Assistant // @match https://www.lkong.com/forum/* // @match https://www.lkong.com/thread/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant unsafeWindow // @run-at document-start // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/570967/%E9%BE%99%E7%A9%BA%E4%BF%A1%E6%81%AF%E9%99%8D%E5%99%AA%E5%99%A8%20v041.user.js // @updateURL https://update.greasyfork.icu/scripts/570967/%E9%BE%99%E7%A9%BA%E4%BF%A1%E6%81%AF%E9%99%8D%E5%99%AA%E5%99%A8%20v041.meta.js // ==/UserScript== (function() { 'use strict'; // ================== 1. 数据存储与加载 ================== const STORAGE_KEY_USERS = 'lkong_blocked_users'; const STORAGE_KEY_TITLE_KEYWORDS = 'lkong_blocked_title_keywords'; const STORAGE_KEY_REPLY_KEYWORDS = 'lkong_blocked_reply_keywords'; let blockedUsers = []; // { userId: string, deepBlock: boolean }[] let blockedTitleKeywords = new Set(); let blockedReplyKeywords = new Set(); async function loadBlockedData() { try { const [storedUsersStr, storedTitleKeywords, storedReplyKeywords] = await Promise.all([ GM_getValue(STORAGE_KEY_USERS, '[]'), GM_getValue(STORAGE_KEY_TITLE_KEYWORDS, '[]'), GM_getValue(STORAGE_KEY_REPLY_KEYWORDS, '[]') ]); // --- 用户数据迁移与加载 --- let usersData = JSON.parse(storedUsersStr); if (usersData.length > 0 && typeof usersData[0] === 'string') { // 旧版数据 (string[]), 迁移到新版 ({ userId, deepBlock }) console.log('LKong Blocker: 检测到旧版用户数据,正在迁移...'); blockedUsers = usersData.map(userId => ({ userId: userId, deepBlock: false })); await saveBlockedData(); // 迁移后立即保存 } else { blockedUsers = usersData; } blockedTitleKeywords = new Set(JSON.parse(storedTitleKeywords)); blockedReplyKeywords = new Set(JSON.parse(storedReplyKeywords)); } catch (e) { console.error('LKong Blocker: 加载噪声名单失败', e); blockedUsers = []; blockedTitleKeywords = new Set(); blockedReplyKeywords = new Set(); } } async function saveBlockedData() { try { await Promise.all([ GM_setValue(STORAGE_KEY_USERS, JSON.stringify(blockedUsers)), GM_setValue(STORAGE_KEY_TITLE_KEYWORDS, JSON.stringify(Array.from(blockedTitleKeywords))), GM_setValue(STORAGE_KEY_REPLY_KEYWORDS, JSON.stringify(Array.from(blockedReplyKeywords))) ]); } catch(e) { console.error('LKong Blocker: 保存噪声名单失败', e); } } // ================== 辅助功能:API获取用户名 ================== async function fetchUserName(userId) { if (!userId) return null; const query = { "operationName": "ViewUserContentsPage", "variables": { "uid": parseInt(userId, 10), "page": 1, "isDigest": false }, "query": "query ViewUserContentsPage($uid: Int!, $isDigest: Boolean!, $page: Int) {\n content: userReplies(uid: $uid, isDigest: $isDigest, page: $page) {\n author {\n name\n __typename\n }\n __typename\n }\n}" }; try { const response = await fetch("https://api.lkong.com/api", { "method": "POST", "credentials": "include", // ★★★ 必须加上这一行 ★★★ "headers": { "content-type": "application/json" }, "body": JSON.stringify(query) }); const json = await response.json(); // 如果未登录,返回特定错误以便调试 if (json.errors) { console.warn('LK-Blocker: API返回权限错误,可能登录状态失效', json.errors); return null; } const replies = json?.data?.content; if (replies && replies.length > 0 && replies[0].author) { return replies[0].author.name; } return null; // 有权限,但用户真的没发过贴,或者被全站屏蔽了 } catch (error) { console.error("LK-Blocker: API获取用户名网络异常", error); return null; } } // ================== 2. 核心处理逻辑 ================== // --- 2.1 论坛列表页:过滤帖子标题 --- function processThreadsData(threads) { if (!threads || !Array.isArray(threads)) return; for (const thread of threads) { const uid = (thread.author?.uid || thread.authorid)?.toString(); const tid = thread.tid?.toString(); const title = thread.subject || ''; if (!uid || !tid) continue; const threadLink = document.querySelector(`a[href*="/thread/${tid}"]`); if (!threadLink) continue; // Find the containing thread item using multiple fallbacks (avoid fragile css-xxxxx) const threadItem = threadLink.closest('.css-760i8n') || threadLink.closest('div[class*="thread"]') || threadLink.closest('article') || threadLink.closest('li') || threadLink.closest('[data-tid]'); if (!threadItem || threadItem.dataset.lkProcessed === 'true') continue; threadItem.dataset.lkProcessed = 'true'; if (blockedUsers.some(user => user.userId === uid)) { console.log(`LKong Blocker: 已按用户 [${uid}] 净化帖子 (TID: ${tid})。`); threadItem.style.display = 'none'; continue; } const currentTitle = title || threadLink.textContent; for (const keyword of blockedTitleKeywords) { if (keyword && currentTitle.includes(keyword)) { console.log(`LKong Blocker: 已按标题关键词 [${keyword}] 净化帖子 (标题: ${currentTitle})。`); threadItem.style.display = 'none'; threadItem.dataset.lkKeywordBlocked = 'true'; break; } } if (threadItem.dataset.lkKeywordBlocked === 'true') continue; let authorContainer = threadItem.querySelector('.author'); if (!authorContainer) { // fallback to author link or nearest container const authorLink = threadItem.querySelector('a[href^="/user/"]') || threadItem.querySelector('a[href*="author="]'); authorContainer = authorLink ? (authorLink.closest('div') || authorLink) : null; } if (authorContainer) addBlockButton(authorContainer, uid, threadItem); } } // --- 列表页添加按钮:点击后调用API获取名字再屏蔽 --- function addBlockButton(anchorElement, userId, threadItem) { if (anchorElement.querySelector('.lk-block-btn')) return; const blockButton = document.createElement('a'); blockButton.href = '#'; blockButton.textContent = '[净化]'; blockButton.className = 'lk-block-btn'; blockButton.style.cssText = 'margin-left: 8px; font-size: 12px; color: #999;'; blockButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // ============ 1. 优先尝试从 DOM 提取用户名 ============ let domUserName = ''; if (threadItem) { // 根据你提供的 HTML 结构: .author 下的第一个链接通常是用户名 const authorLink = threadItem.querySelector('.author a[href*="/user/"]'); if (authorLink) { domUserName = authorLink.textContent.trim(); } else { // 备用:搜索结果页有时候结构不一样,或者是 avatar const avatarImg = threadItem.querySelector('.ant-avatar img, img[class*="avatar"]'); if (avatarImg && avatarImg.alt) { domUserName = avatarImg.alt; } } } // =================================================== // 如果提取到了名字,就显示名字;否则显示ID const displayName = domUserName || `ID:${userId}`; confirmationModal.show(`确定要净化用户: 【${displayName}】 吗?\n该用户的帖子将从列表中消失。`, async () => { let finalName = domUserName; // ============ 2. 如果DOM没抓到,才请求API ============ if (!finalName) { try { const apiName = await fetchUserName(userId); if (apiName) finalName = apiName; } catch(err) { console.error(err); } } // 默认值 if (!finalName) finalName = '未知用户'; // ============ 3. 保存 ============ if (!blockedUsers.some(u => u.userId === userId)) { blockedUsers.push({ userId: userId, deepBlock: false, userName: finalName }); await saveBlockedData(); // 隐藏当前行 if (threadItem) { threadItem.style.display = 'none'; // 有时候列表由虚线分隔,把分隔线也隐藏可能更好看,但这取决于具体CSS if(threadItem.nextElementSibling && threadItem.nextElementSibling.tagName === 'HR') { threadItem.nextElementSibling.style.display = 'none'; } } } }); }); anchorElement.appendChild(blockButton); } // --- 2.2 帖子详情页:过滤回帖内容 --- function processPost(postElement) { // 使用两个状态:pending表示处理中,true表示已彻底处理 if (postElement.dataset.lkPostProcessed === 'true' || postElement.dataset.lkPostProcessed === 'pending') { return; } postElement.dataset.lkPostProcessed = 'pending'; let retryCount = 0; const maxRetries = 50; // 轮询15次,每次间隔100毫秒 (总等待约 1.5 秒),给React框架挂载元素的时间 function attemptExtraction() { let userId = null; // 尝试第一种链接: a[href*="?author="] (只看TA) const authorFilterLink = postElement.querySelector('a[href*="?author="]'); if (authorFilterLink) { try { // 加入第二个参数保证如果取到相对路径也能正常解析 const url = new URL(authorFilterLink.href, window.location.origin); userId = url.searchParams.get('author'); } catch (e) { /* 忽略无效URL */ } } // 尝试第二种: a[href^="/user/"] (用户主页链接) if (!userId) { const userProfileLink = postElement.querySelector('a[href^="/user/"]'); if (userProfileLink) { userId = userProfileLink.href.split('/').pop(); } } // 【核心修复】:如果没有获取到ID,并且还没有超时,我们再稍微等一等页面挂载DOM if (!userId && retryCount < maxRetries) { retryCount++; setTimeout(attemptExtraction, 1000); return; } // 到这一步,无论成败,代表彻底完成了检索操作 postElement.dataset.lkPostProcessed = 'true'; // ============ 后面才是正式的过滤/执行逻辑 ============ if (userId) { // 执行添加屏蔽按钮 addPurifyButtons(postElement, userId); const blockedUser = blockedUsers.find(u => u.userId === userId); if (blockedUser && blockedUser.deepBlock) { console.log(`LKong Blocker: 已按用户 [${userId}] (深度屏蔽) 净化此楼层。`); postElement.style.display = 'none'; return; // 用户屏蔽优先,直接阻断 } } else { console.log(`LKong Blocker: 未能提取到楼层发帖人ID (可能是帖子架构异常或网络太慢),该楼层仅应用关键词过滤。`); } // 关键词屏蔽检查 (在用户未被屏蔽 或 取不到用户的退拽保护下执行) const contentDiv = postElement.querySelector('.main-content'); if (contentDiv) { const contentText = contentDiv.textContent || ''; for (const keyword of blockedReplyKeywords) { if (keyword && contentText.includes(keyword)) { console.log(`LKong Blocker: 已按回帖关键词 [${keyword}] 净化此楼层。`); postElement.style.display = 'none'; return; } } } // 最后添加折叠按钮 addFoldingFeature(postElement); } // 启动获取检测逻辑 attemptExtraction(); } // --- 2.3 帖子详情页:添加净化按钮 --- function addPurifyButtons(postElement, userId) { // 1. 定位操作栏 (放按钮的地方) const findActionsContainer = (el) => { if(!el) return null; // 尝试在当前元素或子元素找 let target = el.querySelector && el.querySelector('.css-9ph873, .css-1feda4v, .post-actions, .actions'); if (target) return target; // 尝试去父级找(适应 React 渲染层级偏差) if (el.parentElement) { target = el.parentElement.querySelector('.css-9ph873, .css-1feda4v, .post-actions, .actions'); if (target) return target; } return null; }; const actionsContainer = findActionsContainer(postElement); if (!actionsContainer || actionsContainer.querySelector('.lk-purify-btn')) return; const reportBtn = actionsContainer.querySelector('.css-rhu9bd, .css-j5tknx, a[title*="举报"]'); const createButton = (text, className, titlePrefix, isDeep) => { const btn = document.createElement('div'); btn.className = `lk-action-wrapper ${className}`; btn.innerHTML = `${text}`; btn.title = `${titlePrefix} (ID:${userId})`; btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); let foundName = ''; // ============ 核心逻辑:向上雷达扫描 ============ let pointer = btn.parentElement; let levels = 0; // 向上爬 7 层,这足以跳出 .main-content 进入 .main-content-wrap while(pointer && levels < 7) { // 场景 1: 楼主层布局 (Top-level layout) // 对应你提供的第二段代码: .user-wrapper > strong const userWrapper = pointer.querySelector('.user-wrapper strong'); if (userWrapper) { foundName = userWrapper.textContent; console.log(`LK-Blocker: 命中楼主布局 (Level ${levels})`); break; } // 场景 2: 普通回复布局 (Reply layout) // 对应代码: .left-area > .user > h2 // 只要容器内有 .left-area,我们就在这个范围内细找 const leftArea = pointer.querySelector('.left-area'); if (leftArea) { const h2 = leftArea.querySelector('.user h2'); // 排除只显示"楼主"字样的情况,必须找用户名 if (h2) { // 优先取 h2 下的第一个 span(它最干净,不含其他杂质) const nameSpan = h2.querySelector('span:first-child'); if (nameSpan) { foundName = nameSpan.textContent.trim(); } else { // 只有实在找不到span,才拿整个h2,但要做字符串清洗 // 暴力清洗:只要空格前的第一部分 foundName = h2.textContent.split(/[\s\n\t]+|楼主|Lv\./)[0].trim(); } console.log(`LK-Blocker: 命中回帖布局 (Level ${levels})`); break; } } // 场景 3: 响应式/移动端窄屏布局 // 有时候 .left-area 没了,但头像还在,图片 alt 是最稳的 const avatarImg = pointer.querySelector('img.ant-avatar-image, .ant-avatar img'); if (avatarImg && avatarImg.alt && avatarImg.alt.length > 0) { // 防止取到 "avatar" 这种无效文本,只取看似名字的 if (avatarImg.alt !== 'avatar') { foundName = avatarImg.alt; // 不break,因为上面两个文本查找更精准,这个作为备选先存着 // 但如果是楼主布局,通常上面那个user-wrapper已经命中了 } } pointer = pointer.parentElement; levels++; } // 数据清洗 if (foundName) foundName = foundName.replace(/[\r\n\t]/g, '').trim(); // =========================================== const displayName = foundName || `ID:${userId}`; const actionText = isDeep ? '深度净化' : '净化'; const warningText = isDeep ? '他的所有主题和回帖都将被隐藏。' : '他的所有主题都将被隐藏。'; const confirmMsg = `确定要${actionText}用户: 【${displayName}】\n(ID: ${userId}) 吗?\n${warningText}`; confirmationModal.show(confirmMsg, async () => { const existingUser = blockedUsers.find(u => u.userId === userId); if (existingUser) { if (existingUser.deepBlock !== isDeep) { existingUser.deepBlock = isDeep; if (foundName) existingUser.userName = foundName; await saveBlockedData(); } } else { const nameToSave = foundName || '未知用户'; blockedUsers.push({ userId: userId, deepBlock: isDeep, userName: nameToSave }); await saveBlockedData(); } alert(`用户 ${displayName} 已被${isDeep ? '深度' : ''}净化。`); window.location.reload(); }); }); return btn; }; const purifyBtn = createButton('净化', 'lk-purify-btn', '净化', false); const deepPurifyBtn = createButton('深度净化', 'lk-deep-purify-btn', '深度净化', true); if (reportBtn) { actionsContainer.insertBefore(purifyBtn, reportBtn); actionsContainer.insertBefore(deepPurifyBtn, reportBtn); } else { actionsContainer.appendChild(purifyBtn); actionsContainer.appendChild(deepPurifyBtn); } } function initPostObserver() { // Use multiple fallbacks to find a stable root to observe const targetNode = document.querySelector('div.css-xt623x') || document.querySelector('div.css-1gnk3bx') || document.getElementById('__next') || document.querySelector('div[data-reactroot]'); if (!targetNode) { setTimeout(initPostObserver, 500); return; } const findPostForContent = (contentEl) => { return contentEl.closest('.css-1pp9a0y') || contentEl.closest('div.posts-ancor') || contentEl.closest('div[class*="post"]') || contentEl.closest('article') || contentEl.closest('li') || contentEl.closest('[data-floor]') || contentEl.parentElement; }; // 1. 首次加载时处理已有帖子:以 .thread-content 为锚点,找到所属帖子容器 targetNode.querySelectorAll('.main-content').forEach(contentEl => { const postEl = findPostForContent(contentEl); if (postEl) processPost(postEl); }); // 2. 创建观察器处理动态加载:当有新节点加入时,查找其子树中的 .thread-content const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.matches && node.matches('.main-content')) { const postEl = findPostForContent(node); if (postEl) processPost(postEl); } node.querySelectorAll && node.querySelectorAll('.main-content').forEach(contentEl => { const postEl = findPostForContent(contentEl); if (postEl) processPost(postEl); }); } }); } } }); observer.observe(targetNode, { childList: true, subtree: true }); console.log("LKong Blocker: 帖子内容监视器已启动。"); } // --- 2.4 帖子折叠功能 --- function addFoldingFeature(postElement) { const content = postElement.querySelector('.main-content'); // Based on user feedback, the button should be next to the floor number (e.g., #1, #2). // The floor number is inside a div with the class 'right-area'. const rightArea = postElement.querySelector('.right-area'); if (!content || !rightArea || rightArea.querySelector('.fold-button')) { return; // Skip if essential elements are missing or button exists } if (content.offsetHeight > 600) { const foldButton = document.createElement('a'); foldButton.textContent = '折叠'; foldButton.className = 'fold-button'; foldButton.href = 'javascript:void(0);'; foldButton.dataset.folded = 'false'; // Prepend the button to the 'right-area' div to place it before the floor number. rightArea.prepend(foldButton); foldButton.addEventListener('click', (e) => { e.preventDefault(); const isFolded = foldButton.dataset.folded === 'true'; if (isFolded) { content.classList.remove('folded'); foldButton.textContent = '折叠'; foldButton.dataset.folded = 'false'; } else { content.classList.add('folded'); foldButton.textContent = '展开'; foldButton.dataset.folded = 'true'; } }); } } function createFoldAllButton() { if (document.getElementById('fold-all-btn')) return; const foldAllBtn = document.createElement('div'); foldAllBtn.id = 'fold-all-btn'; foldAllBtn.textContent = '一键折叠'; document.body.appendChild(foldAllBtn); foldAllBtn.addEventListener('click', () => { const foldButtons = document.querySelectorAll('.fold-button'); foldButtons.forEach(button => { if (button.dataset.folded === 'false') { button.click(); } }); }); } // ================== 3. 数据拦截 (用于论坛列表页) ================== // (这部分无需修改) function handleApiResponse(responseText) { try { const data = JSON.parse(responseText); const threads = data?.data?.threads; if (threads) { setTimeout(() => processThreadsData(threads), 500); } } catch (e) { /* 忽略 */ } } const originalXhrSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(...args) { this.addEventListener('load', function() { if (this.responseURL?.includes('api.lkong.com/api') && this.status === 200) { if (this.responseType === 'blob') this.response.text().then(handleApiResponse); else handleApiResponse(this.responseText); } }); return originalXhrSend.apply(this, args); }; const originalFetch = unsafeWindow.fetch; unsafeWindow.fetch = async function(...args) { const response = await originalFetch(...args); const url = args[0] instanceof Request ? args[0].url : args[0]; if (typeof url === 'string' && url.includes('api.lkong.com/api')) { response.clone().text().then(handleApiResponse); } return response; }; // ================== 4. UI 与 DOM 相关操作 ================== function handleInitialData() { const nextDataScript = document.getElementById('__NEXT_DATA__'); if (!nextDataScript) return; try { const data = JSON.parse(nextDataScript.textContent); const allThreads = data?.props?.pageProps?.threads || []; if (data?.props?.pageProps?.source?.topThreads) { allThreads.push(...data.props.pageProps.source.topThreads); } if (allThreads.length > 0) setTimeout(() => processThreadsData(allThreads), 100); } catch (e) { console.error('LKong Blocker: 解析 __NEXT_DATA__ 失败', e); } } // (修改) 扩展管理UI以支持三类屏蔽 function createManagerUI() { GM_addStyle(` /* --- General --- */ #lk-block-manager-btn {position: fixed; bottom: 135px; right: 20px; background-color: #007bff; color: white; padding: 10px 15px; border-radius: 5px; cursor: pointer; z-index: 9999; font-size: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } #lk-block-manager-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.2); } #lk-block-manager-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 550px; max-width: 95vw; max-height: 90vh; background-color: #fcfcfc; border: 1px solid #e0e0e0; border-radius: 12px; z-index: 10000; box-shadow: 0 5px 20px rgba(0,0,0,0.2); display: none; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } /* --- Header --- */ .lk-manager-header { padding: 16px 24px; border-bottom: 1px solid #e0e0e0; } .lk-manager-header h3 { margin: 0; text-align: center; font-size: 18px; font-weight: 600; color: #212121; } /* --- Body & Tabs --- */ .lk-manager-body { padding: 0 24px 24px 24px; overflow-y: auto; flex-grow: 1; } .lk-tabs { display: flex; border-bottom: 1px solid #e0e0e0; margin: 0 -24px 20px -24px; /* Extend to panel edges */ padding: 0 24px; } .lk-tab { padding: 12px 16px; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; /* Overlap the container border */ font-size: 15px; color: #666; transition: all 0.2s ease; } .lk-tab:hover { background-color: #f5f5f5; color: #333; } .lk-tab.active { color: #2196F3; font-weight: 600; border-bottom-color: #2196F3; } .lk-tab-content { display: none; } .lk-tab-content.active { display: block; } /* --- Form Elements --- */ .lk-manager-body textarea { width: 100%; box-sizing: border-box; height: 250px; margin-bottom: 10px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; resize: vertical; border: 1px solid #ccc; border-radius: 6px; padding: 8px 12px; font-size: 13px; transition: border-color 0.2s ease, box-shadow 0.2s ease; } .lk-manager-body textarea:focus { outline: none; border-color: #2196F3; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); } .lk-manager-body p { font-size: 13px; color: #666; margin-top: 0; margin-bottom: 10px; line-height: 1.5; } /* --- Footer --- */ .lk-manager-footer { padding: 16px 24px; border-top: 1px solid #e0e0e0; display: flex; justify-content: flex-end; background-color: #f5f5f5; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; gap: 12px; } /* --- Unified Button Styles --- */ .lk-btn { padding: 9px 18px; cursor: pointer; border: 1px solid transparent; border-radius: 6px; font-size: 14px; font-weight: 500; text-align: center; transition: all 0.2s ease-in-out; -webkit-font-smoothing: antialiased; } .lk-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.08); } .lk-btn:active { transform: translateY(0); box-shadow: none; filter: brightness(0.95); } /* Button Color Modifiers */ .lk-btn.lk-btn-primary { background-color: #4CAF50; color: white; border-color: #4CAF50; } .lk-btn.lk-btn-secondary { background-color: #2196F3; color: white; border-color: #2196F3; } .lk-btn.lk-btn-danger { background-color: #f44336; color: white; border-color: #f44336; } .lk-btn.lk-btn-default { background-color: #f0f0f0; color: #333; border-color: #ccc; } .lk-btn.lk-btn-default:hover { background-color: #e0e0e0; border-color: #bbb; } /* --- User List Specifics --- */ #lk-blocked-users-list { height: 220px; overflow-y: auto; border: 1px solid #e0e0e0; padding: 8px; margin-bottom: 15px; border-radius: 6px; background-color: #fff; } .lk-user-item { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; border-radius: 4px; transition: background-color 0.2s ease; } .lk-user-item:not(:last-child) { border-bottom: 1px solid #f0f0f0; } .lk-user-item:hover { background-color: #f5f5f5; } .lk-user-item label { flex-grow: 1; display: flex; align-items: center; /* Vertical alignment for checkbox */ cursor: pointer; } .lk-user-item input[type="checkbox"] { margin-right: 12px; width: 16px; height: 16px; accent-color: #2196F3; } .lk-user-item .user-id { font-family: "SF Mono", "Fira Code", "Consolas", monospace; font-size: 14px; color: #333; } .lk-user-item .remove-btn { margin-left: 15px; color: #f44336; cursor: pointer; font-weight: bold; font-size: 20px; line-height: 1; transition: color 0.2s ease, transform 0.2s ease; } .lk-user-item .remove-btn:hover { color: #d32f2f; transform: scale(1.2); } /* --- Add User Form & Import/Export --- */ .lk-add-user-form { display: flex; gap: 10px; margin-top: 15px; } .lk-add-user-form input { flex-grow: 1; padding: 9px 12px; border: 1px solid #ccc; border-radius: 6px; transition: border-color 0.2s ease, box-shadow 0.2s ease; } .lk-add-user-form input:focus { outline: none; border-color: #2196F3; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); } .lk-add-user-form button { /* Uses .lk-btn styles now */ flex-shrink: 0; } .lk-import-export-section { margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0; /* Replaces
勾选“深度屏蔽”后,该用户的回帖也会在帖子页面被隐藏。
批量导入/导出 (格式: userId,deepBlock):
每行一个关键词。帖子标题包含任意一个词都会在列表页被隐藏。
每行一个关键词。帖子内的回帖如果包含任意一个词,该楼层将被隐藏。
您确定要执行此操作吗?