百度贴吧终极增强套件Pro

修复版:彻底解决翻页后过滤失效问题,性能优化,增加网络优化预加载,部分来自(Grok AI)

  1. // ==UserScript==
  2. // @name 百度贴吧终极增强套件Pro
  3. // @namespace http://tampermonkey.net/
  4. // @version 7.54
  5. // @description 修复版:彻底解决翻页后过滤失效问题,性能优化,增加网络优化预加载,部分来自(Grok AI)
  6. // @author YourName
  7. // @match *://tieba.baidu.com/p/*
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_addStyle
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js
  12. // @require https://cdn.jsdelivr.net/npm/lz-string@1.4.4/libs/lz-string.min.js
  13. // @connect tieba.baidu.com
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // 一、关键修复点说明:
  21. // 1. 增强AJAX拦截,覆盖XMLHttpRequest
  22. // 2. 改进MutationObserver监听动态内容
  23. // 3. 持久化屏蔽词配置到localStorage
  24. // 4. 监听翻页按钮点击触发过滤
  25. // 5. 使用标记优化过滤性能
  26. // 6. 修复控制面板拖动不跟手问题
  27.  
  28. // 二、性能优化常量
  29. const DEBOUNCE_LEVEL = { QUICK: 100, COMPLEX: 500 };
  30. const LOG_LEVEL = GM_getValue('logLevel', 'verbose');
  31. const MAX_LOG_ENTRIES = 100;
  32. const DATA_VERSION = 2;
  33. const CACHE_TTL = 60000;
  34.  
  35. // 三、网络优化预加载资源列表
  36. const preloadList = [
  37. '/static/emoji.png',
  38. '/static/theme-dark.css'
  39. ];
  40.  
  41. // 四、增强的日志管理
  42. const logBuffer = { script: [], pageState: [], pageBehavior: [], userActions: [] };
  43. const originalConsole = {
  44. log: console.log.bind(console),
  45. warn: console.warn.bind(console),
  46. error: console.error.bind(console)
  47. };
  48.  
  49. function logWrapper(category, level, ...args) {
  50. const debugMode = GM_getValue('debugMode', true);
  51. const levelMap = { ERROR: 1, WARN: 2, LOG: 3 };
  52. const currentLevel = levelMap[level.toUpperCase()];
  53. const minLevel = levelMap[LOG_LEVEL.toUpperCase()] || 3;
  54.  
  55. if (!debugMode || (currentLevel && minLevel < currentLevel)) return;
  56.  
  57. const timestamp = new Date().toISOString();
  58. const formattedArgs = args.map(arg => {
  59. if (typeof arg === 'object') {
  60. try { return JSON.stringify(arg); }
  61. catch { return String(arg); }
  62. }
  63. return String(arg);
  64. }).join(' ');
  65.  
  66. const message = `[${timestamp}] [${level}] ${formattedArgs}`;
  67. logBuffer[category].push(message);
  68. if (logBuffer[category].length > MAX_LOG_ENTRIES) logBuffer[category].shift();
  69. originalConsole[level.toLowerCase()](message);
  70. }
  71.  
  72. const customConsole = {
  73. log: (...args) => logWrapper('script', 'LOG', ...args),
  74. warn: (...args) => logWrapper('script', 'WARN', ...args),
  75. error: (...args) => logWrapper('script', 'ERROR', ...args)
  76. };
  77.  
  78. // 五、通用工具类
  79. class DomUtils {
  80. static safeQuery(selector, parent = document) {
  81. try {
  82. return parent.querySelector(selector) || null;
  83. } catch (e) {
  84. customConsole.error(`无效选择器: ${selector}`, e);
  85. return null;
  86. }
  87. }
  88.  
  89. static safeQueryAll(selector, parent = document) {
  90. try {
  91. return parent.querySelectorAll(selector);
  92. } catch (e) {
  93. customConsole.error(`无效选择器: ${selector}`, e);
  94. return [];
  95. }
  96. }
  97. }
  98.  
  99. // 六、配置管理类
  100. class ConfigManager {
  101. static get defaultFilterSettings() {
  102. return {
  103. hideInvalid: true,
  104. hideSpam: true,
  105. spamKeywords: ["顶", "沙发", "签到"],
  106. whitelist: [],
  107. blockedElements: [],
  108. tempBlockedElements: [],
  109. autoExpandImages: true,
  110. blockType: 'perm',
  111. blockAds: true,
  112. enhanceImages: true,
  113. linkifyVideos: true,
  114. darkMode: false,
  115. showHiddenFloors: false
  116. };
  117. }
  118.  
  119. static get defaultPanelSettings() {
  120. return {
  121. width: 320,
  122. minHeight: 100,
  123. maxHeight: '90vh',
  124. position: { x: 20, y: 20 },
  125. scale: 1.0,
  126. minimized: true
  127. };
  128. }
  129.  
  130. static getFilterSettings() {
  131. const raw = GM_getValue('settings');
  132. const settings = raw ? decompressSettings(raw) : this.defaultFilterSettings;
  133. const savedKeywords = loadConfig();
  134. if (savedKeywords.length > 0) settings.spamKeywords = savedKeywords;
  135. return settings;
  136. }
  137.  
  138. static getPanelSettings() {
  139. return GM_getValue('panelSettings', this.defaultPanelSettings);
  140. }
  141.  
  142. static updateFilterSettings(newSettings) {
  143. GM_setValue('settings', compressSettings({ ...this.defaultFilterSettings, ...newSettings }));
  144. saveConfig(newSettings.spamKeywords);
  145. }
  146.  
  147. static updatePanelSettings(newSettings) {
  148. GM_setValue('panelSettings', { ...this.defaultPanelSettings, ...newSettings });
  149. }
  150. }
  151.  
  152. // 七、本地存储压缩与迁移
  153. function compressSettings(settings) {
  154. return LZString.compressToUTF16(JSON.stringify(settings));
  155. }
  156.  
  157. function decompressSettings(data) {
  158. try {
  159. return JSON.parse(LZString.decompressFromUTF16(data));
  160. } catch {
  161. return ConfigManager.defaultFilterSettings;
  162. }
  163. }
  164.  
  165. function saveConfig(keywords) {
  166. localStorage.setItem('filterConfig', JSON.stringify(keywords));
  167. customConsole.log('保存屏蔽词配置到 localStorage:', keywords);
  168. }
  169.  
  170. function loadConfig() {
  171. return JSON.parse(localStorage.getItem('filterConfig')) || [];
  172. }
  173.  
  174. function migrateSettings() {
  175. const storedVer = GM_getValue('dataVersion', 1);
  176. if (storedVer < DATA_VERSION) {
  177. const old = GM_getValue('settings');
  178. if (storedVer === 1 && old) {
  179. const decompressed = decompressSettings(old);
  180. const newSettings = { ...ConfigManager.defaultFilterSettings, ...decompressed };
  181. GM_setValue('settings', compressSettings(newSettings));
  182. }
  183. GM_setValue('dataVersion', DATA_VERSION);
  184. }
  185. }
  186.  
  187. // 八、错误边界
  188. class ErrorBoundary {
  189. static wrap(fn, context) {
  190. return function (...args) {
  191. try {
  192. return fn.apply(context, args);
  193. } catch (e) {
  194. customConsole.error(`Error in ${fn.name}:`, e);
  195. context.showErrorToast?.(`${fn.name}出错`, e);
  196. return null;
  197. }
  198. };
  199. }
  200. }
  201.  
  202. // 九、事件管理
  203. const listenerMap = new WeakMap();
  204. function addSafeListener(element, type, listener) {
  205. const wrapped = e => { try { listener(e); } catch (err) { customConsole.error('Listener error:', err); } };
  206. element.addEventListener(type, wrapped);
  207. listenerMap.set(listener, wrapped);
  208. }
  209.  
  210. function removeSafeListener(element, type, listener) {
  211. const wrapped = listenerMap.get(listener);
  212. if (wrapped) {
  213. element.removeEventListener(type, wrapped);
  214. listenerMap.delete(listener);
  215. }
  216. }
  217.  
  218. // 十、性能监控类
  219. class PerformanceMonitor {
  220. static instance;
  221. constructor() {
  222. this.metrics = { memoryUsage: [], processSpeed: [], networkRequests: [] };
  223. this.maxMetrics = 100;
  224. }
  225.  
  226. static getInstance() {
  227. if (!PerformanceMonitor.instance) PerformanceMonitor.instance = new PerformanceMonitor();
  228. return PerformanceMonitor.instance;
  229. }
  230.  
  231. recordMemory() {
  232. if ('memory' in performance) {
  233. const used = performance.memory.usedJSHeapSize;
  234. this.metrics.memoryUsage.push(used);
  235. if (this.metrics.memoryUsage.length > this.maxMetrics) this.metrics.memoryUsage.shift();
  236. logWrapper('pageState', 'LOG', `Memory usage: ${Math.round(used / 1024 / 1024)} MB`);
  237. }
  238. }
  239.  
  240. recordProcessSpeed(time) {
  241. this.metrics.processSpeed.push(time);
  242. if (this.metrics.processSpeed.length > this.maxMetrics) this.metrics.processSpeed.shift();
  243. logWrapper('pageState', 'LOG', `Process speed: ${time.toFixed(2)} ms`);
  244. }
  245. }
  246.  
  247. // 十一、智能空闲任务调度
  248. const idleQueue = [];
  249. let idleCallback = null;
  250.  
  251. function scheduleIdleTask(task) {
  252. idleQueue.push(task);
  253. if (!idleCallback) {
  254. idleCallback = requestIdleCallback(processIdleTasks, { timeout: 1000 });
  255. }
  256. }
  257.  
  258. function processIdleTasks(deadline) {
  259. while (deadline.timeRemaining() > 0 && idleQueue.length) {
  260. idleQueue.shift()();
  261. }
  262. if (idleQueue.length) {
  263. idleCallback = requestIdleCallback(processIdleTasks, { timeout: 1000 });
  264. } else {
  265. idleCallback = null;
  266. }
  267. }
  268.  
  269. // 十二、网络优化策略
  270. function prefetchResources() {
  271. preloadList.forEach(url => {
  272. const link = document.createElement('link');
  273. link.rel = 'prefetch';
  274. link.href = url;
  275. document.head.appendChild(link);
  276. });
  277. customConsole.log('预加载资源完成:', preloadList);
  278. }
  279.  
  280. const cacheStore = new Map();
  281.  
  282. function smartFetch(url) {
  283. const cached = cacheStore.get(url);
  284. if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
  285. customConsole.log('从缓存读取:', url);
  286. return Promise.resolve(cached.data);
  287. }
  288. return fetch(url)
  289. .then(res => res.json())
  290. .then(data => {
  291. cacheStore.set(url, { data, timestamp: Date.now() });
  292. customConsole.log('缓存新数据:', url);
  293. return data;
  294. })
  295. .catch(err => {
  296. customConsole.error('请求失败:', url, err);
  297. throw err;
  298. });
  299. }
  300.  
  301. // 十三、帖子过滤类
  302. class PostFilter {
  303. constructor() {
  304. this.settings = ConfigManager.getFilterSettings();
  305. this.postContainer = DomUtils.safeQuery('.l_post_list') || DomUtils.safeQuery('.pb_list') || document.body;
  306. this.postsCache = new Map();
  307. this.spamPosts = new Set();
  308. this.originalOrder = new Map();
  309. this.applyStyles();
  310. this.saveOriginalOrder();
  311. this.applyFilters = ErrorBoundary.wrap(this.applyFilters, this);
  312. this.applyFilters();
  313. this.autoExpandImages();
  314. this.observeDOMChanges();
  315. this.handlePagination();
  316. this.startSpamEnforcer();
  317. this.blockAds();
  318. this.interceptAjax();
  319. if (this.settings.linkifyVideos) this.linkifyVideos();
  320. this.cleanupMemory();
  321. customConsole.log('PostFilter 初始化完成');
  322. }
  323.  
  324. applyStyles() {
  325. GM_addStyle(`
  326. .l_post {
  327. transition: opacity 0.3s ease;
  328. overflow: hidden;
  329. }
  330. .spam-hidden {
  331. display: none !important;
  332. opacity: 0;
  333. }
  334. .invalid-hidden { display: none !important; }
  335. .pb_list.filtering, .l_post_list.filtering {
  336. position: relative;
  337. opacity: 0.8;
  338. }
  339. .pb_list.filtering::after, .l_post_list.filtering::after {
  340. content: "过滤中...";
  341. position: absolute;
  342. top: 20px;
  343. left: 50%;
  344. transform: translateX(-50%);
  345. color: #666;
  346. }
  347. `);
  348. }
  349.  
  350. saveOriginalOrder() {
  351. const posts = DomUtils.safeQueryAll('.l_post', this.postContainer);
  352. customConsole.log('保存帖子原始顺序,检测到帖子数量:', posts.length);
  353. posts.forEach(post => {
  354. const data = this.safeGetPostData(post);
  355. if (!this.originalOrder.has(data.pid)) {
  356. this.originalOrder.set(data.pid, { pid: data.pid, floor: data.floor, element: post.cloneNode(true) });
  357. }
  358. });
  359. customConsole.log('保存帖子原始顺序完成,数量:', this.originalOrder.size);
  360. logWrapper('pageBehavior', 'LOG', `Saved original order, posts: ${this.originalOrder.size}`);
  361. }
  362.  
  363. applyFilters(nodes = DomUtils.safeQueryAll('.l_post:not(.filtered)', this.postContainer)) {
  364. logWrapper('script', 'LOG', 'Starting filter process', `Keywords: ${this.settings.spamKeywords}`);
  365. customConsole.log('开始应用过滤器,帖子数量:', nodes.length);
  366.  
  367. const startTime = performance.now();
  368. this.postContainer.classList.add('filtering');
  369. let hiddenCount = 0;
  370.  
  371. const keywords = this.settings.spamKeywords
  372. .map(k => k.trim())
  373. .filter(k => k.length > 0);
  374. const regex = keywords.length > 0
  375. ? new RegExp(`(${keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})`, 'i')
  376. : null;
  377.  
  378. nodes.forEach(post => {
  379. const data = this.safeGetPostData(post);
  380. const pid = data.pid;
  381.  
  382. if (!post || post.classList.contains('filtered')) return;
  383.  
  384. post.style.display = '';
  385. post.classList.remove('spam-hidden', 'invalid-hidden');
  386.  
  387. if (this.settings.hideInvalid && !data.content) {
  388. post.classList.add('invalid-hidden');
  389. hiddenCount++;
  390. logWrapper('pageBehavior', 'LOG', `Hid invalid post: ${pid}`);
  391. if (this.settings.showHiddenFloors) {
  392. post.classList.remove('invalid-hidden');
  393. hiddenCount--;
  394. logWrapper('pageBehavior', 'LOG', `Restored invalid post: ${pid}`);
  395. }
  396. } else if (this.settings.hideSpam && data.content && regex && regex.test(data.content)) {
  397. post.classList.add('spam-hidden');
  398. post.style.display = 'none';
  399. this.spamPosts.add(pid);
  400. hiddenCount++;
  401. const matchedKeyword = keywords.find(k => data.content.toLowerCase().includes(k.toLowerCase())) || 'unknown';
  402. logWrapper('pageBehavior', 'LOG', `Hid spam post: ${pid}, Keyword: ${matchedKeyword}, Content: ${data.content.slice(0, 50)}...`);
  403. }
  404.  
  405. post.classList.add('filtered');
  406. this.postsCache.set(pid, true);
  407. });
  408.  
  409. const blockedElements = [...(this.settings.blockedElements || []), ...(this.settings.tempBlockedElements || [])];
  410. blockedElements.forEach(selector => {
  411. DomUtils.safeQueryAll(selector + ':not(.filtered)', this.postContainer).forEach(el => {
  412. const pid = el.dataset.pid || `temp_${Date.now()}`;
  413. if (!this.postsCache.has(pid)) {
  414. el.classList.add('spam-hidden');
  415. el.style.display = 'none';
  416. hiddenCount++;
  417. logWrapper('pageBehavior', 'LOG', `Hid blocked element: ${selector}`);
  418. el.classList.add('filtered');
  419. this.postsCache.set(pid, true);
  420. }
  421. });
  422. });
  423.  
  424. setTimeout(() => {
  425. this.postContainer.classList.remove('filtering');
  426. if (hiddenCount > 0) this.showToast(`已屏蔽 ${hiddenCount} 条水贴`, 'success');
  427. customConsole.log(`[LOG] 翻页后过滤完成,处理帖子数:${nodes.length}`);
  428. logWrapper('pageBehavior', 'LOG', `Filter completed, processed posts: ${nodes.length}, hidden: ${hiddenCount}`);
  429. }, 500);
  430.  
  431. const endTime = performance.now();
  432. PerformanceMonitor.getInstance().recordProcessSpeed(endTime - startTime);
  433. return hiddenCount;
  434. }
  435.  
  436. startSpamEnforcer() {
  437. const enforce = () => {
  438. const allPosts = DomUtils.safeQueryAll('.l_post', this.postContainer);
  439. allPosts.forEach(post => {
  440. const data = this.safeGetPostData(post);
  441. const pid = data.pid;
  442. if (this.spamPosts.has(pid) && document.body.contains(post) && post.style.display !== 'none') {
  443. post.style.display = 'none';
  444. post.classList.add('spam-hidden');
  445. logWrapper('pageBehavior', 'LOG', `Re-enforced spam hiding for post: ${pid}`);
  446. }
  447. });
  448. setTimeout(enforce, 500);
  449. };
  450. enforce();
  451. customConsole.log('Spam enforcer 已启动');
  452. }
  453.  
  454. autoExpandImages(nodes = DomUtils.safeQueryAll('.replace_tip:not(.expanded)', this.postContainer)) {
  455. if (!this.settings.autoExpandImages) return;
  456. customConsole.log('开始自动展开图片,图片数量:', nodes.length);
  457. logWrapper('pageBehavior', 'LOG', 'Auto expanding images');
  458. nodes.forEach(tip => {
  459. if (tip.style.display !== 'none' && !tip.classList.contains('expanded')) {
  460. const rect = tip.getBoundingClientRect();
  461. tip.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: rect.left, clientY: rect.top }));
  462. tip.classList.add('expanded');
  463. const img = DomUtils.safeQuery('img', tip.closest('.replace_div'));
  464. if (this.settings.enhanceImages && img) this.enhanceImage(img);
  465. this.postsCache.set(tip.dataset.pid || `img_${Date.now()}`, true);
  466. logWrapper('pageBehavior', 'LOG', `Expanded image for post: ${tip.dataset.pid || 'unknown'}`);
  467. }
  468. });
  469. }
  470.  
  471. updateFilters() {
  472. this.settings = ConfigManager.getFilterSettings();
  473. customConsole.log('更新过滤器配置,屏蔽词:', this.settings.spamKeywords);
  474. logWrapper('script', 'LOG', 'Updating filters with new settings');
  475. this.postsCache.clear();
  476. this.spamPosts.clear();
  477. this.saveOriginalOrder();
  478. this.applyFilters();
  479. this.autoExpandImages();
  480. this.blockAds();
  481. this.restoreOriginalOrder();
  482. if (this.settings.linkifyVideos) this.linkifyVideos();
  483. }
  484.  
  485. observeDOMChanges() {
  486. const observer = new MutationObserver(mutations => {
  487. mutations.forEach(mut => {
  488. if (mut.addedNodes.length) {
  489. this.applyFilters();
  490. this.autoExpandImages();
  491. logWrapper('pageBehavior', 'LOG', 'DOM change detected, filters reapplied');
  492. }
  493. });
  494. });
  495. observer.observe(document.body, { childList: true, subtree: true });
  496. customConsole.log('MutationObserver 已初始化');
  497. }
  498.  
  499. handlePagination() {
  500. addSafeListener(document, 'click', e => {
  501. if (e.target.closest('.pagination a, .pager a')) {
  502. customConsole.log('检测到翻页按钮点击');
  503. logWrapper('userActions', 'LOG', 'Pagination button clicked');
  504. setTimeout(() => {
  505. this.applyFilters();
  506. this.autoExpandImages();
  507. }, 500);
  508. }
  509. });
  510. customConsole.log('翻页按钮监听已初始化');
  511. }
  512.  
  513. interceptAjax() {
  514. const originalFetch = window.fetch;
  515. window.fetch = async (url, options) => {
  516. customConsole.log('拦截到 fetch 请求:', url);
  517. const response = await originalFetch(url, options);
  518. if (/tieba\.baidu\.com\/p\/\d+(\?pn=\d+)?$/.test(url)) {
  519. customConsole.log('检测到翻页 fetch 请求:', url);
  520. logWrapper('pageBehavior', 'LOG', `Page change detected via fetch: ${url}`);
  521. setTimeout(() => {
  522. this.postsCache.clear();
  523. this.spamPosts.clear();
  524. this.saveOriginalOrder();
  525. this.applyFilters();
  526. this.autoExpandImages();
  527. this.blockAds();
  528. this.restoreOriginalOrder(true);
  529. }, 500);
  530. }
  531. return response;
  532. };
  533.  
  534. const originalOpen = XMLHttpRequest.prototype.open;
  535. XMLHttpRequest.prototype.open = function() {
  536. this.addEventListener('load', () => {
  537. if (this.responseURL.includes('tieba.baidu.com/p/')) {
  538. customConsole.log('检测到翻页 XMLHttpRequest 请求:', this.responseURL);
  539. logWrapper('pageBehavior', 'LOG', `Page change detected via XMLHttpRequest: ${this.responseURL}`);
  540. setTimeout(() => {
  541. this.postsCache.clear();
  542. this.spamPosts.clear();
  543. this.saveOriginalOrder();
  544. this.applyFilters();
  545. this.autoExpandImages();
  546. this.blockAds();
  547. this.restoreOriginalOrder(true);
  548. }, 300);
  549. }
  550. });
  551. originalOpen.apply(this, arguments);
  552. };
  553.  
  554. customConsole.log('AJAX 拦截器已初始化(fetch 和 XMLHttpRequest)');
  555. }
  556.  
  557. blockAds() {
  558. if (!this.settings.blockAds) return;
  559. customConsole.log('开始屏蔽广告');
  560. logWrapper('pageBehavior', 'LOG', 'Blocking advertisements');
  561. const adSelectors = ['.ad_item', '.mediago', '[class*="ad_"]:not([class*="content"])', '.app_download_box', '.right_section .region_bright:not(.content)'];
  562. adSelectors.forEach(selector => {
  563. DomUtils.safeQueryAll(selector + ':not(.filtered)', this.postContainer).forEach(el => {
  564. if (!el.closest('.d_post_content') && !el.closest('.l_container')) {
  565. el.classList.add('spam-hidden');
  566. const pid = el.dataset.pid || `ad_${Date.now()}`;
  567. this.postsCache.set(pid, true);
  568. el.classList.add('filtered');
  569. logWrapper('pageBehavior', 'LOG', `Hid ad element: ${selector}`);
  570. }
  571. });
  572. });
  573. }
  574.  
  575. restoreOriginalOrder(isPageChange = false) {
  576. if (!this.postContainer) return;
  577.  
  578. customConsole.log('开始恢复帖子顺序,原始顺序数量:', this.originalOrder.size);
  579. logWrapper('pageBehavior', 'LOG', `Restoring original order, isPageChange: ${isPageChange}`);
  580.  
  581. const newContainer = this.postContainer.cloneNode(false);
  582. const currentPosts = new Map();
  583. DomUtils.safeQueryAll('.l_post', this.postContainer).forEach(post => {
  584. const data = this.safeGetPostData(post);
  585. currentPosts.set(data.pid, post);
  586. });
  587.  
  588. const sortedOrder = Array.from(this.originalOrder.values())
  589. .sort((a, b) => Number(a.floor) - Number(b.floor));
  590.  
  591. sortedOrder.forEach(item => {
  592. const existingPost = currentPosts.get(item.pid);
  593. if (existingPost) {
  594. newContainer.appendChild(existingPost);
  595. } else {
  596. newContainer.appendChild(item.element.cloneNode(true));
  597. }
  598. });
  599.  
  600. this.postContainer.parentNode.replaceChild(newContainer, this.postContainer);
  601. this.postContainer = newContainer;
  602.  
  603. customConsole.log('帖子顺序恢复完成,数量:', sortedOrder.length);
  604. logWrapper('pageBehavior', 'LOG', `Restored order, posts: ${sortedOrder.length}`);
  605. if (!isPageChange) this.applyFilters();
  606. }
  607.  
  608. linkifyVideos() {
  609. customConsole.log('开始链接化视频');
  610. logWrapper('pageBehavior', 'LOG', 'Linking videos');
  611. const videoRegex = /(?:av\d+|BV\w+)|(?:https?:\/\/(?:www\.)?(youtube\.com|youtu\.be)\/[^\s]+)/gi;
  612. DomUtils.safeQueryAll('.d_post_content:not(.filtered)', this.postContainer).forEach(post => {
  613. const pid = this.safeGetPostData(post).pid;
  614. if (!this.postsCache.has(pid)) {
  615. post.innerHTML = post.innerHTML.replace(videoRegex, match => {
  616. return match.startsWith('http') ? `<a href="${match}" target="_blank">${match}</a>` : `<a href="https://bilibili.com/video/${match}" target="_blank">${match}</a>`;
  617. });
  618. post.classList.add('filtered');
  619. this.postsCache.set(pid, true);
  620. logWrapper('pageBehavior', 'LOG', `Linked videos in post: ${pid}`);
  621. }
  622. });
  623. }
  624.  
  625. enhanceImage(img) {
  626. if (!img) return;
  627. img.style.cursor = 'pointer';
  628. removeSafeListener(img, 'click', this.handleImageClick);
  629. addSafeListener(img, 'click', this.handleImageClick.bind(this));
  630. logWrapper('pageBehavior', 'LOG', `Enhanced image: ${img.src}`);
  631. }
  632.  
  633. handleImageClick() {
  634. const overlay = document.createElement('div');
  635. overlay.className = 'image-overlay';
  636. const largeImg = document.createElement('img');
  637. largeImg.src = this.src;
  638. largeImg.className = 'large-image';
  639. overlay.appendChild(largeImg);
  640. document.body.appendChild(overlay);
  641. addSafeListener(overlay, 'click', () => overlay.remove());
  642. addSafeListener(overlay, 'wheel', e => {
  643. e.preventDefault();
  644. const scale = e.deltaY > 0 ? 0.9 : 1.1;
  645. largeImg.style.transform = `scale(${(parseFloat(largeImg.style.transform?.match(/scale\((.*?)\)/)?.[1]) || 1) * scale})`;
  646. });
  647. logWrapper('userActions', 'LOG', `Clicked to enlarge image: ${this.src}`);
  648. }
  649.  
  650. safeGetPostData(post) {
  651. const pid = post?.dataset?.pid || post?.getAttribute('data-pid') || `temp_${Date.now()}`;
  652. const floor = parseInt(post?.dataset?.floor) || 0;
  653. const contentEle = DomUtils.safeQuery('.d_post_content', post);
  654. const content = contentEle ? contentEle.textContent.trim() : '';
  655. const author = DomUtils.safeQuery('.p_author_name', post)?.textContent?.trim() || '匿名';
  656. return { pid, floor, content, author };
  657. }
  658.  
  659. cleanupMemory() {
  660. setInterval(() => {
  661. const now = Date.now();
  662. for (const [pid, data] of this.postsCache) {
  663. if (typeof data === 'object' && data.timestamp && now - data.timestamp > 300000) {
  664. this.postsCache.delete(pid);
  665. }
  666. }
  667. customConsole.log('清理内存,剩余缓存条目:', this.postsCache.size);
  668. }, 60000);
  669. }
  670.  
  671. showToast(message, type = 'info') {
  672. const toast = document.createElement('div');
  673. toast.textContent = message;
  674. toast.style.cssText = `
  675. position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  676. background: ${type === 'success' ? '#34c759' : '#ff4444'}; color: white;
  677. padding: 10px 20px; border-radius: 5px; z-index: 10001; transition: opacity 0.3s;
  678. `;
  679. document.body.appendChild(toast);
  680. setTimeout(() => {
  681. toast.style.opacity = '0';
  682. setTimeout(() => toast.remove(), 300);
  683. }, 2000);
  684. logWrapper('script', 'LOG', `Showed toast: ${message}`);
  685. }
  686. }
  687.  
  688. // 十四、动态面板类
  689. class DynamicPanel {
  690. constructor() {
  691. this.panel = null;
  692. this.minimizedIcon = null;
  693. this.isDragging = false;
  694. this.dragOccurred = false;
  695. this.lastClickTime = 0;
  696. this.isResizing = false;
  697. this.settings = ConfigManager.getFilterSettings();
  698. this.panelSettings = ConfigManager.getPanelSettings();
  699. this.postFilter = new PostFilter();
  700. this.debugPanel = { show: () => {}, hide: () => {}, update: () => {}, remove: () => {} }; // 简化处理
  701. this.init();
  702. this.applyDarkMode(this.settings.darkMode);
  703. migrateSettings();
  704. if (!GM_getValue('debugMode', true)) this.debugPanel.hide();
  705. customConsole.log('DynamicPanel 初始化完成');
  706. }
  707.  
  708. init() {
  709. this.createPanel();
  710. this.createMinimizedIcon();
  711. document.body.appendChild(this.panel);
  712. document.body.appendChild(this.minimizedIcon);
  713. this.loadContent();
  714. this.setupPanelInteractions();
  715. this.minimizePanel();
  716. if (!this.panelSettings.minimized) this.restorePanel();
  717. this.ensureVisibility();
  718. this.observer = new ResizeObserver(() => this.adjustPanelHeight());
  719. this.observer.observe(this.panel.querySelector('.panel-content'));
  720. this.setupUserActionListeners();
  721. this.setupCleanup();
  722. scheduleIdleTask(() => this.startPerformanceMonitoring());
  723. prefetchResources();
  724. }
  725.  
  726. ensureVisibility() {
  727. this.panel.style.opacity = '1';
  728. this.panel.style.visibility = 'visible';
  729. this.minimizedIcon.style.opacity = '1';
  730. this.minimizedIcon.style.visibility = 'visible';
  731. customConsole.log('Panel visibility ensured at:', this.panelSettings.position);
  732. }
  733.  
  734. createPanel() {
  735. this.panel = document.createElement('div');
  736. this.panel.id = 'enhanced-panel';
  737. GM_addStyle(`
  738. #enhanced-panel {
  739. position: fixed; z-index: 9999; top: ${this.panelSettings.position.y}px; left: ${this.panelSettings.position.x}px;
  740. width: ${this.panelSettings.width}px; min-height: ${this.panelSettings.minHeight}px; max-height: ${this.panelSettings.maxHeight};
  741. background: rgba(255,255,255,0.98); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.1);
  742. transition: opacity 0.3s ease; will-change: left, top; contain: strict; display: none; opacity: 1; visibility: visible; height: auto;
  743. }
  744. #minimized-icon {
  745. position: fixed; z-index: 9999; top: ${this.panelSettings.position.y}px; left: ${this.panelSettings.position.x}px;
  746. width: 32px; height: 32px; background: #ffffff; border-radius: 50%; box-shadow: 0 4px 16px rgba(0,0,0,0.2);
  747. display: block; cursor: pointer; text-align: center; line-height: 32px; font-size: 16px; color: #007bff; overflow: hidden;
  748. }
  749. .panel-header { padding: 16px; border-bottom: 1px solid #eee; user-select: none; display: flex; justify-content: space-between; align-items: center; cursor: move; }
  750. .panel-content { padding: 16px; overflow-y: auto; overscroll-behavior: contain; height: auto; max-height: calc(90vh - 80px); }
  751. .resize-handle { position: absolute; bottom: 0; right: 0; width: 20px; height: 20px; cursor: nwse-resize; background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTUgNUwxNSAxNSIgc3Ryb2tlPSIjOTk5OTk5IiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSAxNUwxNSAxNSIgc3Ryb2tlPSIjOTk5OTk5IiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=') no-repeat center; }
  752. .minimize-btn, .scale-btn { cursor: pointer; padding: 0 8px; }
  753. .minimize-btn:hover, .scale-btn:hover { color: #007bff; }
  754. .setting-group { display: flex; align-items: center; padding: 10px 0; gap: 10px; }
  755. .toggle-switch { position: relative; width: 40px; height: 20px; flex-shrink: 0; }
  756. .toggle-switch input { opacity: 0; width: 0; height: 0; }
  757. .toggle-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; border-radius: 10px; cursor: pointer; transition: background 0.3s; }
  758. .toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; top: 2px; background: white; border-radius: 50%; transition: transform 0.3s; box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
  759. .toggle-switch input:checked + .toggle-slider { background: #34c759; }
  760. .toggle-switch input:checked + .toggle-slider:before { transform: translateX(20px); }
  761. .setting-label { flex: 1; font-size: 14px; color: #333; }
  762. body.dark-mode .setting-label { color: #ddd !important; }
  763. select { padding: 4px; border: 1px solid #ddd; border-radius: 4px; }
  764. .divider { height: 1px; background: #eee; margin: 16px 0; }
  765. .tool-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
  766. .tool-card { padding: 12px; background: #f8f9fa; border: 1px solid #eee; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
  767. .tool-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
  768. .metric-grid { display: grid; gap: 12px; }
  769. .progress-bar { height: 4px; background: #e9ecef; border-radius: 2px; overflow: hidden; }
  770. .progress-fill { height: 100%; background: #28a745; width: 0%; transition: width 0.3s ease; }
  771. .block-modal, .keyword-modal, .log-modal, .search-modal {
  772. position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5);
  773. display: flex; justify-content: center; align-items: center; z-index: 10000; pointer-events: auto;
  774. }
  775. .modal-content { background: white; padding: 20px; border-radius: 8px; width: 400px; max-height: 80vh; overflow-y: auto; pointer-events: auto; }
  776. .modal-content p { color: #666; margin: 5px 0 10px; font-size: 12px; }
  777. textarea, input[type="text"] { width: 100%; margin: 10px 0; padding: 8px; border: 1px solid #ddd; resize: vertical; }
  778. .modal-actions { text-align: right; }
  779. .btn-cancel, .btn-save, .btn-block, .btn-undo, .btn-confirm, .btn-search {
  780. padding: 6px 12px; margin: 0 5px; border: none; border-radius: 4px; cursor: pointer; pointer-events: auto;
  781. }
  782. .btn-cancel { background: #eee; }
  783. .btn-save, .btn-block, .btn-undo, .btn-confirm, .btn-search { background: #34c759; color: white; }
  784. .btn-block.active { background: #ff4444; }
  785. .btn-undo { background: #ff9800; }
  786. .hover-highlight { outline: 2px solid #ff4444; outline-offset: 2px; }
  787. .blocked-item { display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 1px solid #eee; }
  788. .blocked-item button { padding: 4px 8px; font-size: 12px; }
  789. .cursor-circle {
  790. position: fixed; width: 20px; height: 20px; background: rgba(128, 128, 128, 0.5); border-radius: 50%;
  791. pointer-events: none; z-index: 10001; transition: transform 0.2s ease;
  792. }
  793. .cursor-circle.confirm { background: rgba(52, 199, 89, 0.8); transform: scale(1.5); transition: transform 0.3s ease, background 0.3s ease; }
  794. body.blocking-mode * { cursor: none !important; }
  795. .performance-info { display: none; }
  796. .highlight-match { background-color: yellow; }
  797. .image-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; justify-content: center; align-items: center; }
  798. .large-image { max-width: 90%; max-height: 90%; cursor: move; transform: translateZ(0); backface-visibility: hidden; }
  799. body.dark-mode, body.dark-mode .wrap1, body.dark-mode .l_container, body.dark-mode .pb_content, body.dark-mode .d_post_content, body.dark-mode .left_section, body.dark-mode .right_section {
  800. background: #222 !important; color: #ddd !important; transition: background 0.3s, color 0.3s;
  801. }
  802. body.dark-mode #enhanced-panel { background: rgba(50,50,50,0.98) !important; color: #ddd !important; }
  803. body.dark-mode a { color: #66b3ff !important; }
  804. @media (max-width: 768px) {
  805. #enhanced-panel { width: 90vw !important; left: 5vw !important; }
  806. .tool-grid { grid-template-columns: 1fr; }
  807. }
  808. .quick-actions { margin-top: 10px; }
  809. .quick-actions button { margin: 5px; padding: 5px 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
  810. `);
  811. this.panel.innerHTML = `
  812. <div class="panel-header"><span>贴吧增强控制台</span><div class="panel-controls"><span class="minimize-btn">—</span><span class="scale-btn" data-scale="0.8">缩小</span><span class="scale-btn" data-scale="1.0">还原</span></div></div>
  813. <div class="panel-content"></div>
  814. <div class="resize-handle"></div>
  815. `;
  816. }
  817.  
  818. createMinimizedIcon() {
  819. this.minimizedIcon = document.createElement('div');
  820. this.minimizedIcon.id = 'minimized-icon';
  821. this.minimizedIcon.textContent = '⚙️';
  822. addSafeListener(this.minimizedIcon, 'click', e => {
  823. const now = Date.now();
  824. if (now - this.lastClickTime > 300 && !this.dragOccurred) {
  825. this.toggleMinimize();
  826. this.lastClickTime = now;
  827. }
  828. this.dragOccurred = false;
  829. e.stopPropagation();
  830. });
  831. }
  832.  
  833. loadContent() {
  834. const content = DomUtils.safeQuery('.panel-content', this.panel);
  835. content.innerHTML = `
  836. <div class="filter-controls">
  837. <h3>📊 智能过滤设置</h3>
  838. <div class="setting-group">
  839. <label class="toggle-switch">
  840. <input type="checkbox" data-setting="debugMode" ${GM_getValue('debugMode', true) ? 'checked' : ''}>
  841. <span class="toggle-slider"></span>
  842. </label>
  843. <span class="setting-label">启用调试模式</span>
  844. </div>
  845. <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="hideInvalid" ${this.settings.hideInvalid ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">隐藏无效楼层</span></div>
  846. <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="hideSpam" ${this.settings.hideSpam ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">屏蔽水贴内容</span><button class="btn-config" data-action="editKeywords">✏️ 编辑关键词</button></div>
  847. <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="autoExpandImages" ${this.settings.autoExpandImages ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">自动展开图片</span></div>
  848. <div class="setting-group">
  849. <button class="btn-block" data-action="toggleBlockMode">🛡️ ${this.isBlockingMode ? '停止选择屏蔽' : '开始选择屏蔽元素'}</button>
  850. <select data-setting="blockType">
  851. <option value="perm" ${this.settings.blockType === 'perm' ? 'selected' : ''}>永久屏蔽</option>
  852. <option value="temp" ${this.settings.blockType === 'temp' ? 'selected' : ''}>临时屏蔽</option>
  853. </select>
  854. </div>
  855. <div class="setting-group"><button class="btn-undo" data-action="showUndoList">🔄 查看并撤回屏蔽</button></div>
  856. <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="blockAds" ${this.settings.blockAds ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">自动屏蔽广告</span></div>
  857. <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="enhanceImages" ${this.settings.enhanceImages ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">图片交互优化</span></div>
  858. <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="linkifyVideos" ${this.settings.linkifyVideos ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">视频链接跳转</span></div>
  859. <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="darkMode" ${this.settings.darkMode ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">黑夜模式</span></div>
  860. <div class="setting-group"><label class="toggle-switch"><input type="checkbox" data-setting="showHiddenFloors" ${this.settings.showHiddenFloors ? 'checked' : ''}><span class="toggle-slider"></span></label><span class="setting-label">显示隐藏楼层</span></div>
  861. </div>
  862. <div class="quick-actions">
  863. <button data-action="toggleAllImages">一键展开/收起图片</button>
  864. </div>
  865. <div class="divider"></div>
  866. <div class="advanced-tools">
  867. <h3>⚙️ 高级工具</h3>
  868. <div class="tool-grid">
  869. <button class="tool-card" data-action="exportSettings"><div class="icon">📤</div><span>导出配置</span></button>
  870. <button class="tool-card" data-action="importSettings"><div class="icon">📥</div><span>导入配置</span></button>
  871. <button class="tool-card" data-action="performanceChart"><div class="icon">📈</div><span>性能图表</span></button>
  872. <button class="tool-card" data-action="quickSearch"><div class="icon">🔍</div><span>快速检索</span></button>
  873. <button class="tool-card" data-action="saveLogs"><div class="icon">💾</div><span>保存日志</span></button>
  874. </div>
  875. </div>
  876. <div class="divider"></div>
  877. <div class="performance-info">
  878. <h3>💻 系统监控</h3>
  879. <div class="metric-grid">
  880. <div class="metric-item"><span class="metric-label">内存占用</span><span class="metric-value" id="mem-usage">0 MB</span><div class="progress-bar"><div class="progress-fill" id="mem-progress"></div></div></div>
  881. <div class="metric-item"><span class="metric-label">处理速度</span><span class="metric-value" id="process-speed">0 ms</span><div class="sparkline" id="speed-chart"></div></div>
  882. </div>
  883. </div>
  884. `;
  885. this.bindEvents();
  886. setTimeout(() => this.adjustPanelHeight(), 50);
  887. }
  888.  
  889. adjustPanelHeight() {
  890. if (this.panelSettings.minimized) return;
  891. const content = DomUtils.safeQuery('.panel-content', this.panel);
  892. const headerHeight = DomUtils.safeQuery('.panel-header', this.panel)?.offsetHeight || 0;
  893. const maxHeight = Math.min(content.scrollHeight + headerHeight + 32, window.innerHeight * 0.9);
  894. this.panel.style.height = `${maxHeight}px`;
  895. }
  896.  
  897. bindEvents() {
  898. DomUtils.safeQueryAll('input[type="checkbox"]', this.panel).forEach(checkbox => {
  899. addSafeListener(checkbox, 'change', () => {
  900. if (checkbox.dataset.setting === 'debugMode') {
  901. GM_setValue('debugMode', checkbox.checked);
  902. if (checkbox.checked) {
  903. this.debugPanel.show();
  904. } else {
  905. this.debugPanel.hide();
  906. }
  907. } else if (checkbox.dataset.setting === 'darkMode') {
  908. this.settings.darkMode = checkbox.checked;
  909. ConfigManager.updateFilterSettings(this.settings);
  910. this.applyDarkMode(checkbox.checked);
  911. } else {
  912. this.settings[checkbox.dataset.setting] = checkbox.checked;
  913. ConfigManager.updateFilterSettings(this.settings);
  914. this.postFilter.updateFilters();
  915. }
  916. logWrapper('userActions', 'LOG', `Toggled ${checkbox.dataset.setting} to ${checkbox.checked}`);
  917. this.adjustPanelHeight();
  918. });
  919. });
  920.  
  921. const blockTypeSelect = DomUtils.safeQuery('[data-setting="blockType"]', this.panel);
  922. if (blockTypeSelect) {
  923. addSafeListener(blockTypeSelect, 'change', e => {
  924. this.settings.blockType = e.target.value;
  925. ConfigManager.updateFilterSettings(this.settings);
  926. logWrapper('userActions', 'LOG', `Changed blockType to ${e.target.value}`);
  927. });
  928. }
  929.  
  930. const actions = {
  931. editKeywords: () => this.showKeywordEditor(),
  932. toggleBlockMode: () => this.toggleBlockMode(),
  933. showUndoList: () => this.showUndoList(),
  934. exportSettings: () => this.exportConfig(),
  935. importSettings: () => this.importConfig(),
  936. performanceChart: () => {
  937. const perfInfo = DomUtils.safeQuery('.performance-info', this.panel);
  938. perfInfo.style.display = perfInfo.style.display === 'block' ? 'none' : 'block';
  939. this.adjustPanelHeight();
  940. logWrapper('userActions', 'LOG', `Toggled performance chart: ${perfInfo.style.display}`);
  941. },
  942. quickSearch: () => this.toggleSearch(),
  943. saveLogs: () => this.showLogSaveDialog(),
  944. toggleAllImages: () => {
  945. const tips = DomUtils.safeQueryAll('.replace_tip', this.postFilter.postContainer);
  946. const allExpanded = Array.from(tips).every(tip => tip.classList.contains('expanded'));
  947. tips.forEach(tip => {
  948. if (allExpanded && tip.classList.contains('expanded')) {
  949. tip.click();
  950. tip.classList.remove('expanded');
  951. } else if (!tip.classList.contains('expanded')) {
  952. tip.click();
  953. tip.classList.add('expanded');
  954. }
  955. });
  956. this.showToast(allExpanded ? '已收起所有图片' : '已展开所有图片', 'success');
  957. logWrapper('userActions', 'LOG', `Toggled all images: ${allExpanded ? 'collapsed' : 'expanded'}`);
  958. }
  959. };
  960.  
  961. DomUtils.safeQueryAll('[data-action]', this.panel).forEach(btn => {
  962. addSafeListener(btn, 'click', () => actions[btn.dataset.action]?.());
  963. });
  964.  
  965. const minimizeBtn = DomUtils.safeQuery('.minimize-btn', this.panel);
  966. if (minimizeBtn) addSafeListener(minimizeBtn, 'click', e => { this.toggleMinimize(); e.stopPropagation(); });
  967. }
  968.  
  969. setupPanelInteractions() {
  970. const header = DomUtils.safeQuery('.panel-header', this.panel);
  971. const resizeHandle = DomUtils.safeQuery('.resize-handle', this.panel);
  972. let startX, startY, startWidth, startHeight, rafId;
  973.  
  974. const onDragStart = (e, isIcon = false) => {
  975. if (e.button !== 0) return; // 只响应左键
  976. this.isDragging = true;
  977. this.dragOccurred = false;
  978. const target = isIcon ? this.minimizedIcon : this.panel;
  979. startX = e.clientX - this.panelSettings.position.x;
  980. startY = e.clientY - this.panelSettings.position.y;
  981. e.preventDefault();
  982. customConsole.log(`Drag started at:`, { x: e.clientX, y: e.clientY, isIcon });
  983. };
  984.  
  985. const updatePosition = (x, y) => {
  986. const target = this.panelSettings.minimized ? this.minimizedIcon : this.panel;
  987. const panelWidth = target.offsetWidth;
  988. const panelHeight = target.offsetHeight;
  989. this.panelSettings.position.x = Math.max(10, Math.min(x - startX, window.innerWidth - panelWidth - 10));
  990. this.panelSettings.position.y = Math.max(10, Math.min(y - startY, window.innerHeight - panelHeight - 10));
  991. target.style.left = `${this.panelSettings.position.x}px`;
  992. target.style.top = `${this.panelSettings.position.y}px`;
  993. customConsole.log('Dragging to:', { x: this.panelSettings.position.x, y: this.panelSettings.position.y, minimized: this.panelSettings.minimized });
  994. };
  995.  
  996. const onDragMove = (e) => {
  997. if (!this.isDragging) return;
  998. this.dragOccurred = true;
  999. cancelAnimationFrame(rafId);
  1000. rafId = requestAnimationFrame(() => updatePosition(e.clientX, e.clientY));
  1001. };
  1002.  
  1003. const onDragEnd = (e) => {
  1004. if (!this.isDragging) return;
  1005. this.isDragging = false;
  1006. cancelAnimationFrame(rafId);
  1007. ConfigManager.updatePanelSettings(this.panelSettings);
  1008. this.adjustPanelHeight();
  1009. customConsole.log('Drag ended, position:', this.panelSettings.position);
  1010. setTimeout(() => { this.dragOccurred = false; }, 100);
  1011. };
  1012.  
  1013. if (header) {
  1014. addSafeListener(header, 'mousedown', (e) => onDragStart(e, false));
  1015. }
  1016. addSafeListener(this.minimizedIcon, 'mousedown', (e) => onDragStart(e, true));
  1017. addSafeListener(document, 'mousemove', onDragMove);
  1018. addSafeListener(document, 'mouseup', onDragEnd);
  1019.  
  1020. if (resizeHandle) {
  1021. addSafeListener(resizeHandle, 'mousedown', (e) => {
  1022. this.isResizing = true;
  1023. startX = e.clientX;
  1024. startY = e.clientY;
  1025. startWidth = this.panelSettings.width;
  1026. startHeight = parseInt(this.panel.style.height) || this.panel.offsetHeight;
  1027. e.preventDefault();
  1028. });
  1029. }
  1030.  
  1031. const onResizeMove = (e) => {
  1032. if (this.isResizing) {
  1033. const newWidth = startWidth + (e.clientX - startX);
  1034. const newHeight = startHeight + (e.clientY - startY);
  1035. this.panelSettings.width = Math.max(200, newWidth);
  1036. this.panel.style.width = `${this.panelSettings.width}px`;
  1037. this.panel.style.height = `${Math.max(200, newHeight)}px`;
  1038. }
  1039. };
  1040.  
  1041. const onResizeEnd = () => {
  1042. if (this.isResizing) {
  1043. this.isResizing = false;
  1044. ConfigManager.updatePanelSettings(this.panelSettings);
  1045. this.adjustPanelHeight();
  1046. customConsole.log('Resize ended, size:', { width: this.panelSettings.width });
  1047. }
  1048. };
  1049.  
  1050. addSafeListener(document, 'mousemove', onResizeMove);
  1051. addSafeListener(document, 'mouseup', onResizeEnd);
  1052.  
  1053. DomUtils.safeQueryAll('.scale-btn', this.panel).forEach(btn => {
  1054. addSafeListener(btn, 'click', e => {
  1055. this.panelSettings.scale = parseFloat(btn.dataset.scale);
  1056. this.panel.style.transform = `scale(${this.panelSettings.scale})`;
  1057. ConfigManager.updatePanelSettings(this.panelSettings);
  1058. this.ensureVisibility();
  1059. this.adjustPanelHeight();
  1060. e.stopPropagation();
  1061. });
  1062. });
  1063.  
  1064. addSafeListener(window, 'resize', () => {
  1065. const target = this.panelSettings.minimized ? this.minimizedIcon : this.panel;
  1066. const panelWidth = target.offsetWidth * (this.panelSettings.scale || 1);
  1067. const panelHeight = target.offsetHeight * (this.panelSettings.scale || 1);
  1068. this.panelSettings.position.x = Math.min(this.panelSettings.position.x, window.innerWidth - panelWidth - 10);
  1069. this.panelSettings.position.y = Math.min(this.panelSettings.position.y, window.innerHeight - panelHeight - 10);
  1070. target.style.left = `${this.panelSettings.position.x}px`;
  1071. target.style.top = `${this.panelSettings.position.y}px`;
  1072. ConfigManager.updatePanelSettings(this.panelSettings);
  1073. customConsole.log('Adjusted position on resize:', this.panelSettings.position);
  1074. });
  1075. }
  1076. setupUserActionListeners() {
  1077. addSafeListener(document, 'click', e => {
  1078. if (!this.panel.contains(e.target) && !this.minimizedIcon.contains(e.target)) {
  1079. logWrapper('userActions', 'LOG', `Clicked on page at (${e.clientX}, ${e.clientY}), Target: ${e.target.tagName}.${e.target.className || ''}`);
  1080. }
  1081. });
  1082. addSafeListener(document, 'scroll', _.debounce(() => {
  1083. logWrapper('userActions', 'LOG', `Scrolled to (${window.scrollX}, ${window.scrollY})`);
  1084. }, DEBOUNCE_LEVEL.QUICK));
  1085. }
  1086.  
  1087. startPerformanceMonitoring() {
  1088. const perfMonitor = PerformanceMonitor.getInstance();
  1089. const update = () => {
  1090. perfMonitor.recordMemory();
  1091. const memUsage = perfMonitor.metrics.memoryUsage.length ? Math.round(_.mean(perfMonitor.metrics.memoryUsage) / 1024 / 1024) : 0;
  1092. const memElement = DomUtils.safeQuery('#mem-usage', this.panel);
  1093. const progElement = DomUtils.safeQuery('#mem-progress', this.panel);
  1094. if (memElement && progElement) {
  1095. memElement.textContent = `${memUsage} MB`;
  1096. progElement.style.width = `${Math.min(memUsage / 100 * 100, 100)}%`;
  1097. }
  1098. this.debugPanel.update({
  1099. posts: DomUtils.safeQueryAll('.l_post', this.postFilter.postContainer).length,
  1100. hidden: DomUtils.safeQueryAll('.spam-hidden', this.postFilter.postContainer).length,
  1101. memory: memUsage,
  1102. cacheSize: this.postFilter.postsCache.size
  1103. });
  1104. requestAnimationFrame(update);
  1105. };
  1106. requestAnimationFrame(update);
  1107. }
  1108.  
  1109. toggleMinimize() {
  1110. if (this.panelSettings.minimized) this.restorePanel();
  1111. else this.minimizePanel();
  1112. ConfigManager.updatePanelSettings(this.panelSettings);
  1113. this.ensureVisibility();
  1114. logWrapper('userActions', 'LOG', `Panel minimized: ${this.panelSettings.minimized}`);
  1115. }
  1116.  
  1117. minimizePanel() {
  1118. this.panel.style.display = 'none';
  1119. this.minimizedIcon.style.display = 'block';
  1120. this.minimizedIcon.style.left = `${this.panelSettings.position.x}px`;
  1121. this.minimizedIcon.style.top = `${this.panelSettings.position.y}px`;
  1122. this.panelSettings.minimized = true;
  1123. this.ensureVisibility();
  1124. customConsole.log('Minimized panel, icon position:', this.panelSettings.position);
  1125. }
  1126.  
  1127. restorePanel() {
  1128. this.panel.style.display = 'block';
  1129. this.minimizedIcon.style.display = 'none';
  1130. this.panel.style.left = `${this.panelSettings.position.x}px`;
  1131. this.panel.style.top = `${this.panelSettings.position.y}px`;
  1132. this.panelSettings.minimized = false;
  1133. this.adjustPanelHeight();
  1134. this.ensureVisibility();
  1135. customConsole.log('Restored panel, position:', this.panelSettings.position);
  1136. }
  1137.  
  1138. toggleBlockMode(event) {
  1139. this.isBlockingMode = !this.isBlockingMode;
  1140. const blockBtn = DomUtils.safeQuery('.btn-block', this.panel);
  1141. blockBtn.textContent = `🛡️ ${this.isBlockingMode ? '停止选择屏蔽' : '开始选择屏蔽元素'}`;
  1142. blockBtn.classList.toggle('active', this.isBlockingMode);
  1143.  
  1144. if (this.isBlockingMode) {
  1145. document.body.classList.add('blocking-mode');
  1146. this.createCursorCircle();
  1147. this.listeners = {
  1148. move: this.moveCursorCircle.bind(this),
  1149. click: this.handleBlockClick.bind(this)
  1150. };
  1151. addSafeListener(document, 'mousemove', this.listeners.move);
  1152. addSafeListener(document, 'click', this.listeners.click);
  1153. } else {
  1154. document.body.classList.remove('blocking-mode');
  1155. this.removeCursorCircle();
  1156. if (this.listeners) {
  1157. removeSafeListener(document, 'mousemove', this.listeners.move);
  1158. removeSafeListener(document, 'click', this.listeners.click);
  1159. this.listeners = null;
  1160. }
  1161. this.removeHighlight();
  1162. this.selectedTarget = null;
  1163. }
  1164. if (event) event.stopPropagation();
  1165. this.adjustPanelHeight();
  1166. logWrapper('userActions', 'LOG', `Toggled block mode: ${this.isBlockingMode}`);
  1167. }
  1168.  
  1169. createCursorCircle() {
  1170. this.cursorCircle = document.createElement('div');
  1171. this.cursorCircle.className = 'cursor-circle';
  1172. document.body.appendChild(this.cursorCircle);
  1173. }
  1174.  
  1175. moveCursorCircle(event) {
  1176. if (!this.isBlockingMode || !this.cursorCircle) return;
  1177. this.cursorCircle.style.left = `${event.clientX - 10}px`;
  1178. this.cursorCircle.style.top = `${event.clientY - 10}px`;
  1179. this.highlightElement(event);
  1180. }
  1181.  
  1182. removeCursorCircle() {
  1183. if (this.cursorCircle && document.body.contains(this.cursorCircle)) document.body.removeChild(this.cursorCircle);
  1184. this.cursorCircle = null;
  1185. }
  1186.  
  1187. highlightElement(event) {
  1188. if (!this.isBlockingMode) return;
  1189. this.removeHighlight();
  1190. const target = event.target;
  1191. if (target === this.panel || this.panel.contains(target) || target.classList.contains('block-modal') || target.closest('.block-modal')) return;
  1192. target.classList.add('hover-highlight');
  1193. }
  1194.  
  1195. removeHighlight() {
  1196. const highlighted = DomUtils.safeQuery('.hover-highlight');
  1197. if (highlighted) highlighted.classList.remove('hover-highlight');
  1198. }
  1199.  
  1200. handleBlockClick(event) {
  1201. if (!this.isBlockingMode) return;
  1202. event.preventDefault();
  1203. event.stopPropagation();
  1204. const target = event.target;
  1205. if (target === this.panel || this.panel.contains(target) || target.classList.contains('block-modal') || target.closest('.block-modal')) return;
  1206. this.selectedTarget = target;
  1207. this.showConfirmDialog(event.clientX, event.clientY);
  1208. }
  1209.  
  1210. showConfirmDialog(x, y) {
  1211. const modal = document.createElement('div');
  1212. modal.className = 'block-modal';
  1213. modal.innerHTML = `
  1214. <div class="modal-content">
  1215. <h3>确认屏蔽</h3>
  1216. <p>确定要屏蔽此元素吗?当前模式:${this.settings.blockType === 'temp' ? '临时' : '永久'}</p>
  1217. <div class="modal-actions">
  1218. <button class="btn-cancel">取消</button>
  1219. <button class="btn-confirm">确定</button>
  1220. </div>
  1221. </div>
  1222. `;
  1223. document.body.appendChild(modal);
  1224. const confirmBtn = DomUtils.safeQuery('.btn-confirm', modal);
  1225. const cancelBtn = DomUtils.safeQuery('.btn-cancel', modal);
  1226. addSafeListener(confirmBtn, 'click', e => {
  1227. e.stopPropagation();
  1228. if (this.selectedTarget) this.blockElement(this.selectedTarget, x, y);
  1229. document.body.removeChild(modal);
  1230. }, { once: true });
  1231. addSafeListener(cancelBtn, 'click', e => {
  1232. e.stopPropagation();
  1233. document.body.removeChild(modal);
  1234. this.toggleBlockMode();
  1235. }, { once: true });
  1236. }
  1237.  
  1238. blockElement(target, x, y) {
  1239. if (!target || !document.body.contains(target)) {
  1240. this.showToast('无效的元素', 'error');
  1241. return;
  1242. }
  1243. const selector = this.getUniqueSelector(target);
  1244. const blockList = this.settings.blockType === 'temp' ? this.settings.tempBlockedElements : this.settings.blockedElements;
  1245. if (!blockList.includes(selector)) {
  1246. blockList.push(selector);
  1247. ConfigManager.updateFilterSettings(this.settings);
  1248. this.postFilter.updateFilters();
  1249. logWrapper('userActions', 'LOG', `Blocked element: ${selector}`, `Type: ${this.settings.blockType}`);
  1250. }
  1251. target.classList.add('spam-hidden');
  1252. if (this.cursorCircle) {
  1253. this.cursorCircle.style.left = `${x - 10}px`;
  1254. this.cursorCircle.style.top = `${y - 10}px`;
  1255. this.cursorCircle.classList.add('confirm');
  1256. setTimeout(() => {
  1257. this.cursorCircle.classList.remove('confirm');
  1258. this.toggleBlockMode();
  1259. }, 300);
  1260. } else {
  1261. this.toggleBlockMode();
  1262. }
  1263. this.adjustPanelHeight();
  1264. }
  1265.  
  1266. getUniqueSelector(element) {
  1267. if (element.id) return `#${element.id}`;
  1268. const path = [];
  1269. let current = element;
  1270. while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.body) {
  1271. let selector = current.tagName.toLowerCase();
  1272. if (current.className) selector += `.${current.className.trim().split(/\s+/).join('.')}`;
  1273. const siblings = Array.from(current.parentNode.children).filter(child => child.tagName === current.tagName);
  1274. if (siblings.length > 1) selector += `:nth-child(${siblings.indexOf(current) + 1})`;
  1275. path.unshift(selector);
  1276. current = current.parentNode;
  1277. }
  1278. return path.join(' > ');
  1279. }
  1280.  
  1281. showKeywordEditor() {
  1282. const modal = document.createElement('div');
  1283. modal.className = 'keyword-modal';
  1284. modal.innerHTML = `
  1285. <div class="modal-content">
  1286. <h3>关键词管理</h3>
  1287. <textarea id="keywordInput">${this.settings.spamKeywords.join('\n')}</textarea>
  1288. <div class="modal-actions">
  1289. <button class="btn-cancel">取消</button>
  1290. <button class="btn-save">保存</button>
  1291. </div>
  1292. </div>
  1293. `;
  1294. document.body.appendChild(modal);
  1295. const textarea = DomUtils.safeQuery('#keywordInput', modal);
  1296. const saveBtn = DomUtils.safeQuery('.btn-save', modal);
  1297. const cancelBtn = DomUtils.safeQuery('.btn-cancel', modal);
  1298.  
  1299. addSafeListener(saveBtn, 'click', () => {
  1300. const keywords = textarea.value.split('\n').map(k => k.trim()).filter(k => k.length > 0);
  1301. if (keywords.length > 0) {
  1302. this.settings.spamKeywords = keywords;
  1303. ConfigManager.updateFilterSettings(this.settings);
  1304. customConsole.log('保存自定义屏蔽词:', keywords);
  1305. logWrapper('userActions', 'LOG', `Updated spam keywords: ${keywords.join(', ')}`);
  1306. this.postFilter.updateFilters();
  1307. document.body.removeChild(modal);
  1308. this.showToast('关键词已更新', 'success');
  1309. } else {
  1310. this.showToast('请至少输入一个关键词', 'error');
  1311. }
  1312. });
  1313. addSafeListener(cancelBtn, 'click', () => document.body.removeChild(modal));
  1314. }
  1315.  
  1316. showUndoList() {
  1317. const modal = document.createElement('div');
  1318. modal.className = 'block-modal';
  1319. const permItems = this.settings.blockedElements.length > 0 ?
  1320. this.settings.blockedElements.map((sel, i) => `
  1321. <div class="blocked-item">
  1322. <span>[永久] ${sel}</span>
  1323. <button class="btn-undo" data-index="${i}" data-type="perm">撤销</button>
  1324. </div>
  1325. `).join('') : '';
  1326. const tempItems = this.settings.tempBlockedElements.length > 0 ?
  1327. this.settings.tempBlockedElements.map((sel, i) => `
  1328. <div class="blocked-item">
  1329. <span>[临时] ${sel}</span>
  1330. <button class="btn-undo" data-index="${i}" data-type="temp">撤销</button>
  1331. </div>
  1332. `).join('') : '';
  1333. const listItems = permItems + tempItems || '<p>暂无屏蔽元素</p>';
  1334. modal.innerHTML = `
  1335. <div class="modal-content">
  1336. <h3>屏蔽元素列表</h3>
  1337. <p>点击“撤销”恢复显示对应元素</p>
  1338. ${listItems}
  1339. <div class="modal-actions">
  1340. <button class="btn-cancel">关闭</button>
  1341. </div>
  1342. </div>
  1343. `;
  1344. document.body.appendChild(modal);
  1345. DomUtils.safeQueryAll('.btn-undo', modal).forEach(btn => {
  1346. addSafeListener(btn, 'click', () => {
  1347. const index = parseInt(btn.dataset.index);
  1348. const type = btn.dataset.type;
  1349. this.undoBlockElement(index, type);
  1350. document.body.removeChild(modal);
  1351. this.showUndoList();
  1352. });
  1353. });
  1354. addSafeListener(DomUtils.safeQuery('.btn-cancel', modal), 'click', () => document.body.removeChild(modal));
  1355. }
  1356.  
  1357. undoBlockElement(index, type) {
  1358. const blockList = type === 'temp' ? this.settings.tempBlockedElements : this.settings.blockedElements;
  1359. if (index >= 0 && index < blockList.length) {
  1360. const selector = blockList[index];
  1361. blockList.splice(index, 1);
  1362. ConfigManager.updateFilterSettings(this.settings);
  1363. this.postFilter.updateFilters();
  1364. DomUtils.safeQueryAll(selector).forEach(el => el.classList.remove('spam-hidden'));
  1365. this.showToast(`已撤销屏蔽: ${selector}`, 'success');
  1366. logWrapper('userActions', 'LOG', `Undid block (${type}): ${selector}`);
  1367. }
  1368. this.adjustPanelHeight();
  1369. }
  1370.  
  1371. exportConfig() {
  1372. const config = { filter: this.settings, panel: this.panelSettings };
  1373. const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json;charset=utf-8' });
  1374. const url = URL.createObjectURL(blob);
  1375. const a = document.createElement('a');
  1376. a.href = url;
  1377. a.download = `tieba_enhance_config_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
  1378. document.body.appendChild(a);
  1379. a.click();
  1380. document.body.removeChild(a);
  1381. URL.revokeObjectURL(url);
  1382. this.showToast('配置已导出', 'success');
  1383. logWrapper('userActions', 'LOG', 'Exported configuration');
  1384. }
  1385.  
  1386. importConfig() {
  1387. const modal = document.createElement('div');
  1388. modal.className = 'keyword-modal';
  1389. modal.innerHTML = `
  1390. <div class="modal-content">
  1391. <h3>导入配置</h3>
  1392. <p>请选择配置文件(JSON格式)</p>
  1393. <input type="file" accept=".json" id="configFileInput" />
  1394. <div class="modal-actions">
  1395. <button class="btn-cancel">取消</button>
  1396. <button class="btn-save">导入</button>
  1397. </div>
  1398. </div>
  1399. `;
  1400. document.body.appendChild(modal);
  1401. const fileInput = DomUtils.safeQuery('#configFileInput', modal);
  1402. addSafeListener(DomUtils.safeQuery('.btn-save', modal), 'click', () => {
  1403. const file = fileInput.files[0];
  1404. if (file) {
  1405. const reader = new FileReader();
  1406. reader.onload = (e) => {
  1407. try {
  1408. const importedConfig = JSON.parse(e.target.result);
  1409. this.settings = { ...ConfigManager.defaultFilterSettings, ...importedConfig.filter };
  1410. this.panelSettings = { ...ConfigManager.defaultPanelSettings, ...importedConfig.panel };
  1411. ConfigManager.updateFilterSettings(this.settings);
  1412. ConfigManager.updatePanelSettings(this.panelSettings);
  1413. this.postFilter.updateFilters();
  1414. this.loadContent();
  1415. if (this.panelSettings.minimized) this.minimizePanel(); else this.restorePanel();
  1416. this.applyDarkMode(this.settings.darkMode);
  1417. this.showToast('配置已导入', 'success');
  1418. logWrapper('userActions', 'LOG', 'Imported configuration');
  1419. } catch (err) {
  1420. this.showToast('配置文件无效', 'error');
  1421. }
  1422. document.body.removeChild(modal);
  1423. };
  1424. reader.readAsText(file);
  1425. } else {
  1426. this.showToast('请选择一个配置文件', 'error');
  1427. }
  1428. });
  1429. addSafeListener(DomUtils.safeQuery('.btn-cancel', modal), 'click', () => document.body.removeChild(modal));
  1430. }
  1431.  
  1432. toggleSearch() {
  1433. const modal = document.createElement('div');
  1434. modal.className = 'search-modal';
  1435. modal.innerHTML = `
  1436. <div class="modal-content">
  1437. <h3>快速检索</h3>
  1438. <p>输入关键词搜索帖子内容(支持正则表达式)</p>
  1439. <input type="text" id="searchInput" placeholder="请输入关键词" />
  1440. <div class="modal-actions">
  1441. <button class="btn-cancel">关闭</button>
  1442. <button class="btn-search">搜索</button>
  1443. </div>
  1444. </div>
  1445. `;
  1446. document.body.appendChild(modal);
  1447. const searchInput = DomUtils.safeQuery('#searchInput', modal);
  1448. addSafeListener(DomUtils.safeQuery('.btn-search', modal), 'click', () => {
  1449. const keyword = searchInput.value.trim();
  1450. if (keyword) this.performSearch(keyword);
  1451. });
  1452. addSafeListener(DomUtils.safeQuery('.btn-cancel', modal), 'click', () => document.body.removeChild(modal));
  1453. searchInput.focus();
  1454. }
  1455.  
  1456. performSearch(keyword) {
  1457. DomUtils.safeQueryAll('.highlight-match', this.postFilter.postContainer).forEach(el => el.replaceWith(el.textContent));
  1458. const posts = DomUtils.safeQueryAll('.d_post_content', this.postFilter.postContainer);
  1459. let regex;
  1460. try {
  1461. regex = new RegExp(keyword, 'gi');
  1462. } catch {
  1463. regex = new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
  1464. }
  1465. let found = false;
  1466. posts.forEach(post => {
  1467. if (regex.test(post.textContent)) {
  1468. post.innerHTML = post.innerHTML.replace(regex, match => `<span class="highlight-match">${match}</span>`);
  1469. post.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
  1470. found = true;
  1471. }
  1472. });
  1473. this.showToast(found ? '搜索完成' : '未找到匹配内容', found ? 'success' : 'error');
  1474. logWrapper('userActions', 'LOG', `Searched for keyword: ${keyword}, found: ${found}`);
  1475. }
  1476.  
  1477. showLogSaveDialog() {
  1478. const modal = document.createElement('div');
  1479. modal.className = 'log-modal';
  1480. modal.innerHTML = `
  1481. <div class="modal-content">
  1482. <h3>保存日志</h3>
  1483. <p>点击“保存”将日志导出为文件(当前最多 ${MAX_LOG_ENTRIES} 条/分类)</p>
  1484. <div class="modal-actions">
  1485. <button class="btn-cancel">取消</button>
  1486. <button class="btn-save">保存</button>
  1487. </div>
  1488. </div>
  1489. `;
  1490. document.body.appendChild(modal);
  1491. addSafeListener(DomUtils.safeQuery('.btn-save', modal), 'click', () => {
  1492. const fullLog = [
  1493. '=== 脚本运行日志 ===', ...logBuffer.script,
  1494. '\n=== 网页运行状态 ===', ...logBuffer.pageState,
  1495. '\n=== 网页行为 ===', ...logBuffer.pageBehavior,
  1496. '\n=== 用户操作 ===', ...logBuffer.userActions
  1497. ].join('\n');
  1498. const blob = new Blob([fullLog], { type: 'text/plain;charset=utf-8' });
  1499. const url = URL.createObjectURL(blob);
  1500. const a = document.createElement('a');
  1501. a.href = url;
  1502. a.download = `tieba_enhance_log_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
  1503. document.body.appendChild(a);
  1504. a.click();
  1505. document.body.removeChild(a);
  1506. URL.revokeObjectURL(url);
  1507. document.body.removeChild(modal);
  1508. this.showToast('日志已保存', 'success');
  1509. logWrapper('userActions', 'LOG', 'Saved logs to file');
  1510. });
  1511. addSafeListener(DomUtils.safeQuery('.btn-cancel', modal), 'click', () => document.body.removeChild(modal));
  1512. }
  1513.  
  1514. setupCleanup() {
  1515. addSafeListener(window, 'beforeunload', () => {
  1516. this.panel.remove();
  1517. this.minimizedIcon.remove();
  1518. this.debugPanel.remove();
  1519. this.observer?.disconnect();
  1520. if (this.listeners) {
  1521. removeSafeListener(document, 'mousemove', this.listeners.move);
  1522. removeSafeListener(document, 'click', this.listeners.click);
  1523. }
  1524. });
  1525. }
  1526.  
  1527. applyDarkMode(enable) {
  1528. if (enable) document.body.classList.add('dark-mode');
  1529. else document.body.classList.remove('dark-mode');
  1530. logWrapper('pageBehavior', 'LOG', `Applied dark mode: ${enable}`);
  1531. }
  1532.  
  1533. showToast(message, type = 'info') {
  1534. const toast = document.createElement('div');
  1535. toast.textContent = message;
  1536. toast.style.cssText = `
  1537. position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  1538. background: ${type === 'success' ? '#34c759' : '#ff4444'}; color: white;
  1539. padding: 10px 20px; border-radius: 5px; z-index: 10001; transition: opacity 0.3s
  1540. position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  1541. background: ${type === 'success' ? '#34c759' : '#ff4444'}; color: white;
  1542. padding: 10px 20px; border-radius: 5px; z-index: 10001; transition: opacity 0.3s;
  1543. `;
  1544. document.body.appendChild(toast);
  1545. setTimeout(() => {
  1546. toast.style.opacity = '0';
  1547. setTimeout(() => toast.remove(), 300);
  1548. }, 2000);
  1549. logWrapper('script', 'LOG', `Showed toast: ${message}`);
  1550. }
  1551.  
  1552. showErrorToast(methodName, error) {
  1553. this.showToast(`${methodName}出错: ${error.message}`, 'error');
  1554. }
  1555. }
  1556.  
  1557. // 十五、初始化
  1558. addSafeListener(document, 'DOMContentLoaded', () => {
  1559. new DynamicPanel();
  1560. customConsole.log('DOM content loaded, script initialized');
  1561. });
  1562.  
  1563. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  1564. setTimeout(() => {
  1565. if (!DomUtils.safeQuery('#enhanced-panel') && !DomUtils.safeQuery('#minimized-icon')) {
  1566. new DynamicPanel();
  1567. customConsole.log('Fallback initialization triggered');
  1568. }
  1569. }, 50);
  1570. }
  1571. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址