// ==UserScript== // @name B站内容屏蔽(bili_rebuild改) // @namespace dreamcenter // @version 4.0.0 // @description B站评论过滤器 · 可视化配置界面 // @license MIT // @match *://*.bilibili.com/video/* // @match *://*.bilibili.com/opus/* // @match *://space.bilibili.com/* // @match *://t.bilibili.com/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function () { 'use strict'; /* 悬浮窗按钮开关 * true = 显示右下角 FILTER 悬浮按钮 * false = 隐藏悬浮按钮(配置面板将无法从页面打开) * 修改后重新加载脚本即可生效 */ const SHOW_FAB = true; /* ================================================================ * 默认配置(首次使用 / 点击「重置」时恢复) * 日常修改请使用页面右下角的 FILTER 面板 * ================================================================ */ const DEFAULT_CONFIG = { // 文本关键词屏蔽 banMap: ['例子'], // 屏蔽指定用户 banUser: ['例子'], // 等级屏蔽(level_0~level_6,level_h = 加闪电六级号) banLevel: ['level_0'], // 表情屏蔽(表情的 alt 值,F12 检查后复制) banEmoji: [], // 屏蔽 @某人 banAt: ['例子'], // 屏蔽全部笔记 banExpand: false, // 调试:控制台日志 logMode: false, // 调试:红色标记而不隐藏 redLine: false, // 悬浮按钮(null = 未自定义,使用默认右下角) fabX: null, // 距视口左侧 px fabTop: null, // 跟随屏幕(开启):距视口顶部 px fabDocY: null, // 钉在网页(关闭):文档纵向位置 px fabFollow: false, // true = 跟随屏幕滚动保持可见 / false = 钉在网页内容上随页面移动 // 主题:auto = 跟随系统 / light = 浅色 / dark = 深色 theme: 'auto', // 全局过滤开关(面板可随时切换) enabled: true, // 正则表达式屏蔽(整条评论文本匹配,支持正则语法) banRegex: [], // 按 UID 屏蔽用户(改昵称也不受影响) banUid: [] }; const STORAGE_KEY = 'bili_rebuild_config_v4'; const LEVELS = ['level_0', 'level_1', 'level_2', 'level_3', 'level_4', 'level_5', 'level_6', 'level_h']; /* ================================================================ * 持久化(GM 存储优先,失败则回退 localStorage) * ================================================================ */ function deepClone(o) { return JSON.parse(JSON.stringify(o)); } // 配置字段白名单与类型校验(防止损坏/恶意备份或旧版存储导致脚本异常) const CONFIG_ARRAY_KEYS = ['banMap', 'banUser', 'banLevel', 'banEmoji', 'banAt', 'banRegex', 'banUid']; const CONFIG_BOOL_KEYS = ['banExpand', 'logMode', 'redLine', 'fabFollow', 'enabled']; const CONFIG_NUM_KEYS = ['fabX', 'fabTop', 'fabDocY']; function sanitizeConfig(obj) { const merged = deepClone(DEFAULT_CONFIG); for (const k of CONFIG_ARRAY_KEYS) if (k in obj && Array.isArray(obj[k])) merged[k] = obj[k].filter(v => typeof v === 'string'); for (const k of CONFIG_BOOL_KEYS) if (k in obj) merged[k] = !!obj[k]; for (const k of CONFIG_NUM_KEYS) if (k in obj && (typeof obj[k] === 'number' || obj[k] === null)) merged[k] = obj[k]; if (obj.theme === 'light' || obj.theme === 'dark' || obj.theme === 'auto') merged.theme = obj.theme; return merged; } function loadConfig() { let raw = null; try { if (typeof GM_getValue === 'function') raw = GM_getValue(STORAGE_KEY, null); } catch (e) { } if (!raw) { try { raw = localStorage.getItem(STORAGE_KEY); } catch (e) { } } if (raw) { try { const obj = JSON.parse(raw); return sanitizeConfig(obj); } catch (e) { } } return deepClone(DEFAULT_CONFIG); } function saveConfig() { const raw = JSON.stringify(config); try { if (typeof GM_setValue === 'function') GM_setValue(STORAGE_KEY, raw); } catch (e) { } try { localStorage.setItem(STORAGE_KEY, raw); } catch (e) { } } let config = loadConfig(); /* ================================================================ * 评论过滤引擎(逻辑与原版一致,数据源改为动态 config) * ================================================================ */ const filterFunction = { bySpan(raw) { for (const key of config.banMap) if (raw.includes(key)) return true; }, byImg(raw) { for (const key of config.banEmoji) if (raw.includes(key)) return true; }, byAt(raw) { for (const key of config.banAt) if (raw.includes(key)) return true; }, bySearch(raw) { for (const key of config.banMap) if (raw.includes(key)) return true; } }; /* ========== 正则屏蔽:异步 + 硬超时(防 ReDoS) ========== */ const REGEX_TIMEOUT_MS = 16; // 单轮正则匹配硬超时(毫秒) // 用于正则添加时的耗时基准文本(大量重复字符,放大回溯开销) const BENCH_TEXT = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + 'B站评论过滤基准' + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; // 常见 ReDoS 危险模式预检(嵌套/重叠量词、双重闭合、交替分组量词等) const DANGEROUS_REGEX = /\([^)]*\)[+*][^)]*\)[+*]|\([^)]*\|[^)]*\)[+*]|\+\+|\*\*|\.\*\.\*|\.\*[+*]/; // 正则匹配在 Web Worker 中执行,主线程用定时器硬超时;超时即终止 Worker 重建,并将该轮正则拉黑 const REGEX_WORKER_SRC = ` self.onmessage = function (e) { var d = e.data, matched = false; for (var i = 0; i < d.patterns.length; i++) { try { if (new RegExp(d.patterns[i]).test(d.text)) { matched = true; break; } } catch (err) {} } self.postMessage({ id: d.id, matched: matched }); };`; let regexWorker = null; let regexWorkerBusy = false; let regexWorkerQueue = []; let regexTaskId = 0; let currentRegexTask = null; let currentRegexTaskId = -1; let regexWorkerTimer = null; let regexWorkerDisabled = false; // Worker 不可用(CSP 限制/创建失败/连续超时)时置真,走同步兜底 let regexTimeoutCount = 0; let regexBlacklist = new Set(); // 曾超时的危险正则(会话级跳过,避免反复拖慢) function ensureRegexWorker() { if (regexWorker) return regexWorker; if (regexWorkerDisabled) return null; try { const blob = new Blob([REGEX_WORKER_SRC], { type: 'application/javascript' }); regexWorker = new Worker(URL.createObjectURL(blob)); regexWorker.onmessage = (ev) => { const d = ev.data || {}; if (d.id !== currentRegexTaskId) return; const task = currentRegexTask; currentRegexTask = null; currentRegexTaskId = -1; regexWorkerBusy = false; clearTimeout(regexWorkerTimer); if (task) task.resolve(!!d.matched); pumpRegexQueue(); }; } catch (e) { regexWorker = null; regexWorkerDisabled = true; } return regexWorker; } // Worker 不可用时的同步兜底:整体时间预算 + 单条超时拉黑 + 跳过危险模式,尽量防 ReDoS function testRegexSyncFallback(text, patterns) { const budgetStart = performance.now(); for (const p of patterns) { if (performance.now() - budgetStart > REGEX_TIMEOUT_MS) break; if (DANGEROUS_REGEX.test(p)) { regexBlacklist.add(p); continue; } // 危险模式直接跳过,避免同步卡死 const t0 = performance.now(); let matched = false; try { matched = new RegExp(p).test(text || ''); } catch (e) { } if (performance.now() - t0 > REGEX_TIMEOUT_MS) regexBlacklist.add(p); if (matched) return true; } return false; } function pumpRegexQueue() { if (regexWorkerBusy || !regexWorkerQueue.length) return; const task = regexWorkerQueue.shift(); const patterns = (task.patterns || []).filter(p => !regexBlacklist.has(p)); if (!patterns.length) { task.resolve(false); setTimeout(pumpRegexQueue, 0); return; } regexWorkerBusy = true; currentRegexTask = task; currentRegexTaskId = ++regexTaskId; const w = ensureRegexWorker(); if (!w) { regexWorkerBusy = false; // Worker 不可用(CSP/创建失败):同步兜底匹配(异步调度避免深递归爆栈) task.resolve(testRegexSyncFallback(task.text, patterns)); setTimeout(pumpRegexQueue, 0); return; } w.postMessage({ id: currentRegexTaskId, text: task.text, patterns: patterns }); regexWorkerTimer = setTimeout(() => { const t = currentRegexTask; currentRegexTask = null; currentRegexTaskId = -1; regexWorkerBusy = false; try { regexWorker.terminate(); } catch (e) { } regexWorker = null; // 不在超时处整轮拉黑(避免误伤正常正则);由同步兜底按单条耗时精准拉黑 regexTimeoutCount++; if (regexTimeoutCount >= 3) regexWorkerDisabled = true; if (t) t.resolve(testRegexSyncFallback(t.text, patterns)); setTimeout(pumpRegexQueue, 0); }, REGEX_TIMEOUT_MS); } // 异步测试:文本是否命中任一正则(硬超时,超时视为未命中) function testRegexAsync(text, patterns) { return new Promise((resolve) => { regexWorkerQueue.push({ text: text || '', patterns: patterns || [], resolve }); pumpRegexQueue(); }); } // 同步测试(仅用于添加正则时的耗时基准,不参与评论过滤) function testRegexSync(text, pattern) { const t0 = performance.now(); try { new RegExp(pattern).test(text); } catch (e) { } return performance.now() - t0; } // 待异步正则检测的评论队列(关键词未命中时才入队) const pendingRegexChecks = []; let regexDraining = false; function drainRegexQueue() { if (regexDraining) return; regexDraining = true; const loop = () => { const item = pendingRegexChecks.shift(); if (!item) { regexDraining = false; return; } testRegexAsync(item.text, item.patterns).then(hit => { if (hit) item.apply(); loop(); }); }; loop(); } // 判断通用评论屏蔽(同步:关键词/表情/@,正则交由异步路径) function judgeIfBanned(raw) { const fullText = (raw.innerText || raw.textContent || '').trim(); const replyMatch = fullText.match(/^回复\s*(@[^\s]+)\s*[::]/); let targetUser = replyMatch ? replyMatch[1] : null; for (let node of raw.childNodes) { if (node.nodeType === Node.TEXT_NODE) { // 暂无此情形 } if (node.tagName === 'SPAN') { const text = node.innerText; if (text && filterFunction.bySpan(text)) return true; } else if (node.tagName === 'IMG') { if (filterFunction.byImg(node.alt)) return true; } else if (node.tagName === 'A') { var type = node.dataset.type; if (type == "mention") { const atText = node.innerText.trim(); if (targetUser && targetUser === atText) { targetUser = null; continue; } if (filterFunction.byAt(atText)) return true; } if (type == "search") { const searchText = node.innerText.trim(); if (searchText && filterFunction.bySearch(searchText)) return true; } } } return false; } function judgeIfLevelBanned(levelImg) { for (const key of config.banLevel) if (levelImg.includes(key)) return true; return false; } function judgeIfExpandBanned(expandEL) { return config.banExpand && !!expandEL; } function judgeIfUserBanned(userName) { for (const key of config.banUser) if (userName.includes(key)) return true; return false; } // 从评论节点中提取 UID:B站新版评论在 #user-avatar 的 data-user-profile-id 上 function getUid(scopeEl) { if (!scopeEl || !scopeEl.shadowRoot) return null; const avatar = scopeEl.shadowRoot.querySelector('#user-avatar'); const v = avatar && (avatar.dataset.userProfileId || avatar.getAttribute('data-user-profile-id')); if (v) return String(v).trim(); // 回退:扫描空间链接 const links = Array.from(scopeEl.shadowRoot.querySelectorAll('a[href]')); for (const a of links) { const m = (a.href || '').match(/(?:space\.bilibili\.com|space\/)(\d+)/); if (m) return m[1]; } return null; } function judgeIfUidBanned(uid) { if (!uid) return false; for (const key of config.banUid) if (uid === key) return true; return false; } // 统一隐藏/标红逻辑 function hideComment(el, textEl) { if (config.logMode) console.log(textEl.innerText); if (config.redLine) textEl.style.color = "red"; else el.style.display = "none"; } function runCore() { // 全局过滤开关:关闭时不做任何屏蔽 if (!config.enabled) return; var reviews = Array.from(document.querySelectorAll("bili-comments")).flatMap(el => el.shadowRoot ? Array.from(el.shadowRoot.querySelectorAll("#feed > bili-comment-thread-renderer")) : []); for (var review of reviews) { if (review.getAttribute('filtered') == null) { try { var comment = review.shadowRoot.querySelector('#comment'); var level = comment.shadowRoot.querySelector("#header > bili-comment-user-info").shadowRoot.querySelector("#user-level > img").src; var uid = getUid(comment); var userInfo = comment.shadowRoot.querySelector("#header > bili-comment-user-info"); var userName = userInfo.shadowRoot.querySelector("#user-name").innerText.trim(); var commentText = comment.shadowRoot.querySelector("#content > bili-rich-text").shadowRoot.querySelector("#contents"); var expandEL = comment.shadowRoot.querySelector("#expand"); review.setAttribute('filtered', true); // 顺序规则:关键词/等级/用户等同步判断优先,命中直接隐藏,不再走正则 if (judgeIfLevelBanned(level) || judgeIfBanned(commentText) || judgeIfExpandBanned(expandEL) || judgeIfUserBanned(userName) || judgeIfUidBanned(uid)) { hideComment(review, commentText); } else if (config.banRegex.length) { const text = (commentText.innerText || commentText.textContent || '').trim(); if (text) pendingRegexChecks.push({ text, patterns: config.banRegex, apply: () => hideComment(review, commentText) }); } } catch (e) { /* B站结构变化时跳过该条,不影响其它评论 */ } } try { var replies = review.shadowRoot.querySelector("#replies > bili-comment-replies-renderer").shadowRoot.querySelectorAll("#expander-contents > bili-comment-reply-renderer:not([filtered])"); for (var reply of replies) { var replyLevel = reply.shadowRoot.querySelector("#main > bili-comment-user-info").shadowRoot.querySelector("#user-level > img").src; var replyUid = getUid(reply); var replyUserInfo = reply.shadowRoot.querySelector("#main > bili-comment-user-info"); var replyUserName = replyUserInfo.shadowRoot.querySelector("#user-name").innerText.trim(); var replyCommentText = reply.shadowRoot.querySelector("#main > bili-rich-text").shadowRoot.querySelector("#contents"); reply.setAttribute('filtered', true); if (judgeIfLevelBanned(replyLevel) || judgeIfBanned(replyCommentText) || judgeIfUserBanned(replyUserName) || judgeIfUidBanned(replyUid)) { hideComment(reply, replyCommentText); } else if (config.banRegex.length) { const text = (replyCommentText.innerText || replyCommentText.textContent || '').trim(); if (text) pendingRegexChecks.push({ text, patterns: config.banRegex, apply: () => hideComment(reply, replyCommentText) }); } } } catch (e) { /* B站结构变化时跳过该楼回复,不影响其它 */ } } // 异步处理正则队列(关键词未命中的评论) drainRegexQueue(); } // 清除过滤标记与样式,便于配置变更后重新过滤 function resetFilterMarks() { const threads = Array.from(document.querySelectorAll("bili-comments")).flatMap(el => el.shadowRoot ? Array.from(el.shadowRoot.querySelectorAll("bili-comment-thread-renderer")) : []); for (const review of threads) { if (review.hasAttribute('filtered')) { review.removeAttribute('filtered'); review.style.display = ''; const comment = review.shadowRoot.querySelector('#comment'); if (comment) { const rich = comment.shadowRoot && comment.shadowRoot.querySelector('#content > bili-rich-text'); if (rich && rich.shadowRoot) { const cc = rich.shadowRoot.querySelector('#contents'); if (cc) cc.style.color = ''; } } } const replyRoot = review.shadowRoot && review.shadowRoot.querySelector('#replies > bili-comment-replies-renderer'); if (replyRoot) { replyRoot.querySelectorAll('bili-comment-reply-renderer[filtered]').forEach(r => { r.removeAttribute('filtered'); r.style.display = ''; const main = r.shadowRoot.querySelector('#main'); if (main) { const rich = main.shadowRoot && main.shadowRoot.querySelector('bili-rich-text'); if (rich && rich.shadowRoot) { const cc = rich.shadowRoot.querySelector('#contents'); if (cc) cc.style.color = ''; } } }); } } } /* ================================================================ * 过滤引擎启动 * ================================================================ */ // 过滤引擎启动(不覆盖页面 window.onload,避免 load 已触发或被他脚本覆盖) function startEngine() { var startMark = false; setInterval(function () { if (startMark) { runCore(); } else { startMark = document.querySelector("bili-comments") != null; } }, 100); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startEngine); } else { startEngine(); } /* ================================================================ * Arknights 军规风可视化配置面板 * ================================================================ */ const UI_CSS = ` :host { all: initial; } * { box-sizing: border-box; margin: 0; padding: 0; } [hidden] { display: none !important; } /* ===== 默认 = 深色主题 ===== */ .ark { --bg-panel: linear-gradient(180deg, rgba(48,54,62,0.97) 0%, rgba(36,42,50,0.97) 100%); --fab-bg: linear-gradient(180deg, rgba(48,54,62,0.98), rgba(36,42,50,0.98)); --text: #f1f3f5; --muted: #8b919b; --muted-2: #5b6066; --border: rgba(255,255,255,0.22); --border-soft: rgba(255,255,255,0.12); --border-faint: rgba(255,255,255,0.08); --layer: rgba(255,255,255,0.14); --layer-hover: rgba(255,255,255,0.06); --chip-bg: rgba(255,255,255,0.06); --chip-hover: rgba(255,255,255,0.14); --accent-bg: #ffffff; --accent-text: #111213; --accent-hover: #d5d8dc; --danger: #ff4757; --info: #8ec8ff; --corner: rgba(255,255,255,0.85); --watermark: rgba(255,255,255,0.035); --shadow: 0 0 24px rgba(0,0,0,0.55); --thumb: rgba(255,255,255,0.22); --thumb-hover: rgba(255,255,255,0.4); --input-bg: rgba(255,255,255,0.06); font-family: "Source Han Sans CN","Source Han Sans SC","Microsoft YaHei","PingFang SC",sans-serif; font-weight: 700; color: var(--text); letter-spacing: 0; -webkit-font-smoothing: antialiased; } /* ===== 浅色变量 ===== */ .ark.light { --bg-panel: linear-gradient(180deg, rgba(250,251,252,0.97) 0%, rgba(233,237,241,0.97) 100%); --fab-bg: linear-gradient(180deg, rgba(250,251,252,0.98), rgba(234,238,242,0.98)); --text: #111213; --muted: #5b6066; --muted-2: #6b7280; --border: rgba(0,0,0,0.24); --border-soft: rgba(0,0,0,0.12); --border-faint: rgba(0,0,0,0.06); --layer: rgba(0,0,0,0.05); --layer-hover: rgba(0,0,0,0.08); --chip-bg: rgba(0,0,0,0.04); --chip-hover: rgba(0,0,0,0.08); --accent-bg: #111213; --accent-text: #f5f7f9; --accent-hover: #2c3238; --danger: #e0382f; --info: #2f6fbf; --corner: rgba(0,0,0,0.75); --watermark: rgba(0,0,0,0.05); --shadow: 0 0 24px rgba(0,0,0,0.18); --thumb: rgba(0,0,0,0.22); --thumb-hover: rgba(0,0,0,0.45); --input-bg: rgba(255,255,255,0.6); } /* ===== 自动:跟随系统浅色 ===== */ @media (prefers-color-scheme: light) { .ark:not(.light):not(.dark) { --bg-panel: linear-gradient(180deg, rgba(250,251,252,0.97) 0%, rgba(233,237,241,0.97) 100%); --fab-bg: linear-gradient(180deg, rgba(250,251,252,0.98), rgba(234,238,242,0.98)); --text: #111213; --muted: #5b6066; --muted-2: #6b7280; --border: rgba(0,0,0,0.24); --border-soft: rgba(0,0,0,0.12); --border-faint: rgba(0,0,0,0.06); --layer: rgba(0,0,0,0.05); --layer-hover: rgba(0,0,0,0.08); --chip-bg: rgba(0,0,0,0.04); --chip-hover: rgba(0,0,0,0.08); --accent-bg: #111213; --accent-text: #f5f7f9; --accent-hover: #2c3238; --danger: #e0382f; --info: #2f6fbf; --corner: rgba(0,0,0,0.75); --watermark: rgba(0,0,0,0.05); --shadow: 0 0 24px rgba(0,0,0,0.18); --thumb: rgba(0,0,0,0.22); --thumb-hover: rgba(0,0,0,0.45); --input-bg: rgba(255,255,255,0.6); } } .fab { position: fixed; right: 24px; bottom: 24px; z-index: 2147483647; width: 64px; height: 40px; display: flex; align-items: center; justify-content: center; background: var(--fab-bg); border: 1px solid var(--border); box-shadow: var(--shadow); cursor: pointer; user-select: none; transition: none; } .fab:hover { border-color: var(--border); opacity: 0.92; } .fab:active { transform: scale(0.92); } .fab.dragging { cursor: grabbing; } .fab .fab-label { font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; color: var(--text); } .fab-corner, .panel-corner { position: absolute; width: 7px; height: 7px; } .fab-corner.tl { top: 0; left: 0; border-top: 2px solid var(--corner); border-left: 2px solid var(--corner); } .fab-corner.tr { top: 0; right: 0; border-top: 2px solid var(--corner); border-right: 2px solid var(--corner); } .fab-corner.br { bottom: 0; right: 0; border-bottom: 2px solid var(--corner); border-right: 2px solid var(--corner); } .fab-corner.bl { bottom: 0; left: 0; border-bottom: 2px solid var(--corner); border-left: 2px solid var(--corner); } .panel { position: fixed; right: 24px; bottom: 72px; z-index: 2147483646; width: 344px; max-height: calc(100vh - 120px); display: flex; flex-direction: column; background: var(--bg-panel); border: 1px solid var(--border); box-shadow: var(--shadow); border-radius: 0; } .panel-corner { border: 1px solid var(--corner); } .panel-corner.tl { top: 0; left: 0; border-top-width: 2px; border-left-width: 2px; } .panel-corner.tr { top: 0; right: 0; border-top-width: 2px; border-right-width: 2px; } .panel-corner.br { bottom: 0; right: 0; border-bottom-width: 2px; border-right-width: 2px; } .panel-corner.bl { bottom: 0; left: 0; border-bottom-width: 2px; border-left-width: 2px; } .watermark { position: absolute; top: 52px; right: 14px; z-index: 0; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 52px; font-weight: 800; letter-spacing: 0; color: var(--watermark); pointer-events: none; } .header { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; cursor: move; touch-action: none; border-bottom: 1px solid var(--border-soft); } .label-strip { display: inline-block; background: var(--accent-bg); color: var(--accent-text); padding: 3px 10px; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; } .icon-btn { width: 24px; height: 24px; border: 1px solid var(--border); background: transparent; color: var(--text); cursor: pointer; transition: 0.2s; font-size: 12px; line-height: 1; display: flex; align-items: center; justify-content: center; } .icon-btn:hover { background: var(--layer); } .icon-btn:active { transform: scale(0.9); } .tabs { display: flex; border-bottom: 1px solid var(--border-soft); } .tab { flex: 1; height: 36px; background: transparent; border: none; border-right: 1px solid var(--border-faint); color: var(--muted); cursor: pointer; font-family: "Source Han Sans CN","Microsoft YaHei",sans-serif; font-weight: 700; font-size: 12px; letter-spacing: 1px; transition: 0.2s; } .tab:last-child { border-right: none; } .tab:active { transform: scale(0.97); } .tab.active { background: var(--accent-bg); color: var(--accent-text); } .content { flex: 1; overflow-y: auto; min-height: 120px; max-height: 380px; } .content::-webkit-scrollbar { width: 8px; } .content::-webkit-scrollbar-thumb { background: var(--thumb); } .content::-webkit-scrollbar-thumb:hover { background: var(--thumb-hover); } .tab-page { position: relative; z-index: 1; } .page-label { display: flex; justify-content: space-between; align-items: center; padding: 12px 12px 6px; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); } .page-label.sub { padding-top: 16px; } .page-label .count { font-family: "Bahnschrift",sans-serif; font-size: 11px; color: var(--info); } .tags { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 12px 8px; } .tag { display: inline-flex; align-items: center; gap: 5px; padding: 4px 6px 4px 9px; background: var(--layer); border: 1px solid var(--border); font-size: 12px; max-width: 220px; transition: 0.2s; } .tag:hover { border-color: var(--text); opacity: 0.95; } .tag .tag-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tag .tag-del { flex: none; width: 16px; height: 16px; border: none; background: transparent; color: var(--muted); cursor: pointer; font-size: 11px; line-height: 1; display: flex; align-items: center; justify-content: center; transition: 0.2s; } .tag .tag-del:hover { color: var(--danger); } .tag .tag-del:active { transform: scale(0.85); } .input-row { display: flex; gap: 8px; padding: 4px 12px 14px; } .input-row input { flex: 1; height: 32px; padding: 0 10px; background: var(--input-bg); border: 1px solid var(--border); color: var(--text); outline: none; font-family: "Source Han Sans CN","Microsoft YaHei",sans-serif; font-weight: 700; font-size: 12px; transition: 0.2s; } .input-row input:focus { border-color: var(--text); } .input-row input::placeholder { color: var(--muted-2); font-weight: 400; } .btn-add { height: 32px; padding: 0 14px; background: var(--accent-bg); color: var(--accent-text); border: none; cursor: pointer; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; transition: 0.2s; } .btn-add:hover { background: var(--accent-hover); } .btn-add:active { transform: scale(0.95); } .level-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; padding: 4px 12px 8px; } .level-chip { height: 32px; display: flex; align-items: center; justify-content: center; background: var(--chip-bg); border: 1px solid var(--border); color: var(--muted); cursor: pointer; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 12px; letter-spacing: 0.5px; transition: 0.2s; } .level-chip:hover { background: var(--chip-hover); } .level-chip:active { transform: scale(0.93); } .level-chip.active { background: var(--accent-bg); color: var(--accent-text); border-color: var(--accent-bg); } .hint { padding: 0 12px 14px; font-size: 11px; color: var(--muted-2); font-weight: 400; } .opt-list { display: flex; flex-direction: column; gap: 4px; padding: 4px 12px 12px; } .opt-row { display: flex; justify-content: space-between; align-items: center; height: 40px; padding: 0 10px; border: 1px solid var(--border-faint); cursor: pointer; transition: 0.2s; user-select: none; } .opt-row:hover { background: var(--layer-hover); } .opt-label { font-size: 12px; } .toggle { flex: none; width: 36px; height: 18px; border: 1px solid var(--border); background: var(--layer); position: relative; cursor: pointer; transition: 0.2s; } .toggle .knob { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; background: var(--muted); transition: 0.2s; } .toggle.on { background: var(--accent-bg); border-color: var(--accent-bg); } .toggle.on .knob { left: 20px; background: var(--accent-text); } .theme-grid { display: flex; gap: 6px; padding: 4px 12px 12px; } .theme-chip { flex: 1; height: 30px; display: flex; align-items: center; justify-content: center; background: var(--chip-bg); border: 1px solid var(--border); color: var(--muted); cursor: pointer; font-family: "Source Han Sans CN","Microsoft YaHei",sans-serif; font-weight: 700; font-size: 12px; letter-spacing: 1px; transition: 0.2s; } .theme-chip:hover { background: var(--chip-hover); } .theme-chip:active { transform: scale(0.95); } .theme-chip.active { background: var(--accent-bg); color: var(--accent-text); border-color: var(--accent-bg); } .backup-actions { display: flex; gap: 6px; padding: 4px 12px 12px; } .backup-actions .btn-reset { flex: 1; height: 30px; font-size: 11px; } .reset-row { padding: 4px 12px 14px; } .btn-reset { width: 100%; height: 34px; background: transparent; border: 1px solid var(--border); color: var(--text); cursor: pointer; font-family: "Source Han Sans CN","Microsoft YaHei",sans-serif; font-weight: 700; font-size: 12px; letter-spacing: 1px; transition: 0.2s; } .btn-reset:hover { color: var(--danger); border-color: var(--danger); } .btn-reset:active { transform: scale(0.95); } .footer { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-top: 1px solid var(--border-soft); } .status { font-size: 11px; color: var(--muted); letter-spacing: 1px; } .ver { font-family: "Bahnschrift",sans-serif; font-size: 10px; color: var(--muted-2); letter-spacing: 1px; } `; const UI_HTML = `