您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
论坛列表显示图片,同时支持discuz搭建的论坛(如吾爱破解)以及phpwind搭建的论坛(如south plus)灯
// ==UserScript== // @name 论坛列表显示图片 // @namespace form_show_images_in_list // @version 1.5 // @description 论坛列表显示图片,同时支持discuz搭建的论坛(如吾爱破解)以及phpwind搭建的论坛(如south plus)灯 // @license MIT // @author Gloduck // @note discuz路径匹配 // @match *://*/forum-*.html // @match *://*/forum-*.html?* // @match *://*/forum.php // @match *://*/forum.php?* // @match *://*/*/forum-*.html // @match *://*/*/forum-*.html?* // @match *://*/*/forum.php // @match *://*/*/forum.php?* // @note phpwind路径匹配 // @match *://*/*/thread.php // @match *://*/*/thread.php?* // @match *://*/thread.php // @match *://*/thread.php?* // @note 1024路径匹配 // @match *://*/*/thread0806.php* // @match *://*/thread0806.php* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; GM_addStyle(` .zoomable-image { cursor: pointer; } .zoomable-image.zoomed { position: fixed; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; background: rgba(0, 0, 0, 0.9); z-index: 9999; } `); // 默认设置 const defaultSettings = { enabled: false, lazyLoad: true, maxImageDisplayCount: 3, requestMaxDelay: 3000, ignoredImagePattern: [] }; // 当前设置变量 let currentSettings = {}; const settingsItems = [ { label: '启用脚本', name: 'enabled', type: 'checkbox' }, { label: '懒加载', name: 'lazyLoad', type: 'checkbox' }, { label: '最大图片显示数量', name: 'maxImageDisplayCount', type: 'number', extraAttrs: 'min="1" max="10"' }, { label: '请求最大延迟(ms)', name: 'requestMaxDelay', type: 'number', extraAttrs: 'min="0" max="10000"' }, { label: '忽略图片', name: 'ignoredImagePattern', type: 'textarea', extraAttrs: 'rows="5" placeholder="一行输入一个,支持URL模式匹配..."', serializeValue: (value) => { return value.trim().split('\n').filter(line => line.trim() !== ''); }, deserializeValue: (value) => { return value ? value.join('\n') : ''; } } ]; const typeHandlers = [ { // 类型名称 name: "discuz", parseArticleElements: () => { return document.querySelectorAll('tbody[id^="normalthread_"]'); }, parseContentLink: (articleElement) => { return articleElement.querySelector('a[onclick="atarget(this)"]')?.href; }, parsePostImage: (link, response) => { const images = []; const pageContent = new DOMParser().parseFromString(response, 'text/html'); const postContent = pageContent.querySelector('div[id^="post_"] .plc'); if (!postContent) { return images; } const imgElements = postContent.querySelectorAll('img'); imgElements.forEach(img => { let imageLink = null; imageLink = img.getAttribute('file'); if (!imageLink) { imageLink = img.getAttribute('src'); } if (imageLink) { images.push(convertPathToAccessible(imageLink, link)); } }); return images; }, insertImageContainer: (articleElement, imageContainer) => { const tbody = document.createElement("tbody"); const tr = document.createElement("tr"); tr.appendChild(imageContainer); tbody.appendChild(tr); insertElementBelow(articleElement, tbody); }, urlPattern: [ "*://*/forum-*.html", "*://*/forum-*.html?*", "*://*/forum.php", "*://*/forum.php?*", "*://*/*/forum-*.html", "*://*/*/forum-*.html?*", "*://*/*/forum.php", "*://*/*/forum.php?*" ], ignoredImagePattern: [ "*://*/*/uc_server/images/*", "*://*/*/static/image/*", "*://*/*/data/avatar/*" ] }, { name: "phpwind", parseArticleElements: () => { return document.querySelectorAll('#ajaxtable tbody:last-of-type tr[align=center]'); }, parseContentLink: (articleElement) => { return articleElement.querySelector('td a')?.href; }, parsePostImage: (link, response) => { const images = []; const pageContent = new DOMParser().parseFromString(response, 'text/html'); const postContent = pageContent.querySelector('.tpc_content'); if (!postContent) { return images; } const imgElements = postContent.querySelectorAll('img'); imgElements.forEach(img => { images.push(convertPathToAccessible(img.src, link)); }); return images; }, insertImageContainer: (articleElement, imageContainer) => { let tr = document.createElement("tr"); tr.align = "center"; let td = document.createElement("td"); td.colSpan = 5; tr.appendChild(td); td.appendChild(imageContainer); insertElementBelow(articleElement, tr); }, urlPattern: [ "*://*/*/thread.php", "*://*/*/thread.php?*", "*://*/thread.php", "*://*/thread.php?*" ], ignoredImagePattern: [ "*://*/images/post/smile/*", ] }, { name: "1024", parseArticleElements: () => { return document.querySelectorAll('tbody[id="tbody"] tr'); }, parseContentLink: (articleElement) => { return articleElement.querySelector('.tal h3 a')?.href; }, parsePostImage: (link, response) => { const images = []; const pageContent = new DOMParser().parseFromString(response, 'text/html'); const postContent = pageContent.querySelector('#conttpc'); if (!postContent) { return images; } const imgElements = postContent.querySelectorAll('img'); imgElements.forEach(img => { let imageLink = null; imageLink = img.getAttribute('ess-data'); if (!imageLink) { imageLink = img.getAttribute('src'); } if (imageLink) { images.push(convertPathToAccessible(imageLink, link)); } }); return images; }, insertImageContainer: (articleElement, imageContainer) => { let tr = document.createElement("tr"); tr.align = "center"; let td = document.createElement("td"); td.colSpan = 5; tr.appendChild(td); td.appendChild(imageContainer); insertElementBelow(articleElement, tr); }, urlPattern: [ "*://*/*/thread0806.php*", "*://*/thread0806.php*" ], ignoredImagePattern: [ ] } ]; function chooseActiveHandler() { for (let handler of typeHandlers) { if (handler.urlPattern.some(pattern => matchUrl(window.location.href, pattern))) { console.log(`激活的配置为:${handler.name}`); return handler; } } return null; } function adjustDefaultSetting(handler) { if (handler.ignoredImagePattern) { defaultSettings.ignoredImagePattern = handler.ignoredImagePattern; } } function enhancementByHandler(handler, settings) { const articleList = handler.parseArticleElements(); articleList.forEach(element => { if (settings.lazyLoad) { lazyEnhancement(element, handler, settings); } else { immediateEnhancement(element, handler, settings); } }) } function lazyEnhancement(element, handler, settings) { window.addEventListener('scroll', throttle(function () { const targetElementRect = element.getBoundingClientRect(); if (targetElementRect.top < window.innerHeight) { handleSingleArticle(element, handler, settings); } }, 200, 500)); } function immediateEnhancement(element, handler, settings) { handleSingleArticle(element, handler, settings); } async function handleSingleArticle(element, handler, settings) { if (element.getAttribute("has_enhanced")) { return; } element.setAttribute("has_enhanced", "true"); let link = handler.parseContentLink(element); if (link == null) { throw new Error("无法解析文章连接"); } link = convertPathToAccessible(link, window.location.href); const articleContent = await httpGetRequest(link, settings.requestMaxDelay); if (!articleContent) { throw new Error("无法获取文章内容"); } let images = handler.parsePostImage(link, articleContent); images = filterArticleImages(images, settings.ignoredImagePattern, settings.maxImageDisplayCount); const imageContainer = generateImageContainer(images); handler.insertImageContainer(element, imageContainer); } function generateImageContainer(images) { const imageDiv = document.createElement("div"); imageDiv.style = "display: flex;"; imageDiv.className = "image_list"; images.forEach(value => { const imgElement = document.createElement("img"); imgElement.src = value; imgElement.style = "max-width: 300px;max-height: 300px;margin-right: 10px" imageDiv.appendChild(imgElement); imgElement.addEventListener('click', function () { var zoomedImg = document.createElement('img'); zoomedImg.src = imgElement.src; zoomedImg.classList.add('zoomable-image', 'zoomed'); zoomedImg.addEventListener('click', function () { document.body.removeChild(zoomedImg); }); document.body.appendChild(zoomedImg); }); }) const htmlDivElement = document.createElement("div"); htmlDivElement.appendChild(imageDiv); return htmlDivElement; } function filterArticleImages(images, ignoredImagePattern, showCount) { return images.filter(img => { return !ignoredImagePattern.some(pattern => { return matchUrl(img, pattern) }); }).slice(0, showCount); } function convertPathToAccessible(path, currentPath) { var url = new URL(path, currentPath); return url.href; } function insertElementBelow(targetElement, newElement) { var parentElement = targetElement.parentNode; parentElement.insertBefore(newElement, targetElement.nextSibling); } function httpGetRequest(url, maxDelay) { return new Promise((resolve, reject) => { const delay = Math.random() * maxDelay; setTimeout(() => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function (response) { resolve(response.responseText); }, onerror: function (error) { reject(error); } }); }, delay); }); } function matchUrl(url, pattern) { if (typeof url !== 'string' || typeof pattern !== 'string' || !pattern) { return false; } // 解析URL let parsedUrl; try { const urlObj = new URL(url); parsedUrl = { protocol: urlObj.protocol, domain: urlObj.hostname, path: urlObj.pathname + urlObj.search + urlObj.hash }; } catch (e) { return false; // URL解析失败 } // 验证模式格式 if (!/^([*]|https?):\/\//.test(pattern)) { return false; } // 转换模式为正则表达式 let regexStr = pattern .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // 转义正则特殊字符 .replace(/\*/g, '.*?') // 将*替换为非贪婪匹配 .replace(/^(\*):\/\//, '(http|https):\/\/'); // 处理*://的情况 // 创建正则表达式并添加锚点 const regex = new RegExp(`^${regexStr}$`); // 组合URL各部分并执行匹配 const fullUrl = parsedUrl.protocol + '//' + parsedUrl.domain + parsedUrl.path; return regex.test(fullUrl); } /** * 节流 * @param func {function} 回调函数 * @param wait 延迟执行时间(ms) * @param mustRun 必须执行时间(ms) * @returns {(function(): void)|*} */ function throttle(func, wait, mustRun) { var timeout, startTime = new Date(); return function () { var context = this, args = arguments, curTime = new Date(); clearTimeout(timeout); // 如果达到了规定的触发时间间隔,触发 handler if (curTime - startTime >= mustRun) { func.apply(context, args); startTime = curTime; // 没达到触发间隔,重新设定定时器 } else { timeout = setTimeout(func, wait); } }; }; // 初始化设置 function initSettings() { const saveSettings = GM_getValue(getSettingName()) ?? {}; settingsItems.forEach(item => { const savedValue = saveSettings[item.name]; currentSettings[item.name] = savedValue !== undefined ? savedValue : defaultSettings[item.name]; }); console.log(`当前脚本设置:${JSON.stringify(currentSettings)}`); } function getSettingName() { return window.location.host + '_settings'; } // 创建设置项(支持所有类型) function createSettingItem(labelText, name, type, value, extraAttrs = '', options = []) { const container = document.createElement('div'); container.style.marginBottom = '1.5rem'; const label = document.createElement('label'); label.style.display = 'block'; label.style.marginBottom = '8px'; label.style.fontWeight = '500'; label.textContent = labelText; container.appendChild(label); switch (type) { case 'checkbox-group': { const group = document.createElement('div'); options.forEach(option => { const wrapper = document.createElement('div'); wrapper.style.marginBottom = '6px'; const input = document.createElement('input'); input.type = 'checkbox'; input.name = `${name}[${option.value}]`; input.id = `${name}_${option.value}`; input.checked = value[option.value] === true; input.style.marginRight = '8px'; const optLabel = document.createElement('label'); optLabel.htmlFor = `${name}_${option.value}`; optLabel.textContent = option.text; wrapper.append(input, optLabel); group.appendChild(wrapper); }); container.appendChild(group); break; } case 'radio-group': { const group = document.createElement('div'); options.forEach(option => { const wrapper = document.createElement('div'); wrapper.style.marginBottom = '6px'; const input = document.createElement('input'); input.type = 'radio'; input.name = name; input.id = `${name}_${option.value}`; input.value = option.value; input.checked = value === option.value; input.style.marginRight = '8px'; const optLabel = document.createElement('label'); optLabel.htmlFor = `${name}_${option.value}`; optLabel.textContent = option.text; wrapper.append(input, optLabel); group.appendChild(wrapper); }); container.appendChild(group); break; } case 'select': { const select = document.createElement('select'); options.forEach(option => { const opt = document.createElement('option'); opt.value = option.value; opt.textContent = option.text; opt.selected = option.value === value; select.appendChild(opt); }); select.name = name; select.id = name; select.style.width = '100%'; select.style.padding = '6px'; select.style.border = '1px solid #ddd'; select.style.borderRadius = '4px'; container.appendChild(select); break; } case 'textarea': { const textarea = document.createElement('textarea'); textarea.name = name; textarea.id = name; textarea.value = value; applyExtraAttrs(textarea, extraAttrs); textarea.style.width = '100%'; textarea.style.padding = '6px'; textarea.style.border = '1px solid #ddd'; textarea.style.borderRadius = '4px'; container.appendChild(textarea); break; } default: { // 处理所有input类型 const input = document.createElement('input'); input.type = type; input.name = name; input.id = name; if (type === 'checkbox') { input.checked = value; } else { input.value = value; } applyExtraAttrs(input, extraAttrs); // 滑块特殊处理 if (type === 'range') { const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.alignItems = 'center'; input.style.width = '80%'; input.style.marginRight = '10px'; const valueDisplay = document.createElement('span'); valueDisplay.textContent = value; valueDisplay.style.width = '20%'; valueDisplay.style.textAlign = 'center'; input.addEventListener('input', () => { valueDisplay.textContent = input.value; }); wrapper.append(input, valueDisplay); container.appendChild(wrapper); } else { input.style.width = '100%'; input.style.padding = '6px'; input.style.border = '1px solid #ddd'; input.style.borderRadius = '4px'; if (type === 'checkbox') { input.style.width = 'auto'; input.style.marginRight = '8px'; const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.alignItems = 'center'; wrapper.append(input, label); container.innerHTML = ''; container.appendChild(wrapper); } else { container.appendChild(input); } } } } return container; } // 应用额外属性 function applyExtraAttrs(element, attrsStr) { if (!attrsStr) return; attrsStr.split(' ').forEach(attr => { const [key, val] = attr.split('='); if (key && val) { element.setAttribute(key, val.replace(/"/g, '')); } }); } function showAlert(type, message, closeTime = 0) { // 类型样式映射 const styleMap = { success: { bg: '#4CAF50', icon: '✓' }, error: { bg: '#F44336', icon: '✕' }, warning: { bg: '#FFC107', icon: '!' } }; // 默认使用警告样式 const style = styleMap[type] || styleMap.warning; // 创建提示框元素 const alertDiv = document.createElement('div'); alertDiv.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 15px 25px; background: ${style.bg}; color: white; border-radius: 4px; box-shadow: 0 2px 15px rgba(0,0,0,0.3); z-index: 999999; display: flex; align-items: center; gap: 12px; font-family: Arial, sans-serif; animation: slideDown 0.3s ease-out; `; // 构建内容(根据是否自动关闭决定是否显示关闭按钮) let content = `<span style="font-weight: bold; font-size: 1.2em;">${style.icon}</span> <span>${message}</span>`; // 当不自动关闭时才显示关闭按钮 if (closeTime <= 0) { content += `<button style="margin-left: 15px; background: rgba(255,255,255,0.3); border: none; color: white; border-radius: 50%; width: 20px; height: 20px; cursor: pointer; transition: background 0.2s;">×</button>`; } alertDiv.innerHTML = content; // 添加到页面 document.body.appendChild(alertDiv); // 关闭按钮事件(仅当存在关闭按钮时) const closeBtn = alertDiv.querySelector('button'); if (closeBtn) { closeBtn.addEventListener('click', () => closeAlert(alertDiv)); // 按钮悬停效果 closeBtn.addEventListener('mouseover', () => { closeBtn.style.background = 'rgba(255,255,255,0.5)'; }); closeBtn.addEventListener('mouseout', () => { closeBtn.style.background = 'rgba(255,255,255,0.3)'; }); } // 自动关闭功能 if (closeTime > 0) { setTimeout(() => closeAlert(alertDiv), closeTime); } if (!document.getElementById('custom-alert-animations')) { const styleSheet = document.createElement('style'); styleSheet.id = 'custom-alert-animations'; styleSheet.textContent = ` @keyframes slideDown { from { transform: translateX(-50%) translateY(-20px); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } } `; document.head.appendChild(styleSheet); } } function closeAlert(alertDiv) { alertDiv.style.opacity = '0'; alertDiv.style.transform = 'translateX(-50%) translateY(-20px)'; setTimeout(() => alertDiv.remove(), 300); } // 创建设置菜单 function createSettingsMenu() { let menu = document.getElementById('tampermonkey-settings-menu'); if (menu) return menu; menu = document.createElement('div'); menu.id = 'tampermonkey-settings-menu'; menu.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 9999; min-width: 300px; max-width: 90%; max-height: 80vh; overflow-y: auto; display: none; `; const title = document.createElement('h3'); title.textContent = '脚本设置'; title.style.cssText = `margin: 0 0 1.5rem 0; text-align: center; padding-bottom: 0.5rem; border-bottom: 1px solid #eee;`; menu.appendChild(title); const form = document.createElement('form'); form.id = 'settings-form'; settingsItems.forEach(item => { const saveValue = currentSettings[item.name]; const parseValue = item.deserializeValue ? item.deserializeValue(saveValue) : saveValue; form.appendChild(createSettingItem( item.label, item.name, item.type, parseValue, item.extraAttrs, item.options )); }); // 创建按钮容器,使按钮同排显示 const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 10px; margin-top: 1rem; `; // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.type = 'submit'; saveBtn.textContent = '保存'; saveBtn.style.cssText = ` flex: 1; padding: 8px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; `; buttonContainer.appendChild(saveBtn); // 重置按钮 const resetBtn = document.createElement('button'); resetBtn.type = 'button'; resetBtn.textContent = '重置'; resetBtn.style.cssText = ` flex: 1; padding: 8px; background: #ffc107; color: #212529; border: none; border-radius: 4px; cursor: pointer; `; resetBtn.addEventListener('click', (e) => { if (resetToDefault()) { hideMenu(); } }); buttonContainer.appendChild(resetBtn); // 关闭按钮 const closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.textContent = '关闭'; closeBtn.style.cssText = ` flex: 1; padding: 8px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; `; closeBtn.addEventListener('click', hideMenu); buttonContainer.appendChild(closeBtn); form.appendChild(buttonContainer); menu.appendChild(form); document.body.appendChild(menu); form.addEventListener('submit', (e) => { e.preventDefault(); if (saveSettings()) { hideMenu(); }; }); return menu; } function resetToDefault() { const confirmReset = confirm('确定要将所有设置恢复到默认值吗?此操作不可撤销。'); if (!confirmReset) { return false; } GM_deleteValue(getSettingName()); initSettings(); showAlert('success', '设置已重置', 3000); return true; } // 保存设置 function saveSettings() { const form = document.getElementById('settings-form'); const toSaveSettings = {}; for (const key in settingsItems) { const item = settingsItems[key]; let rawValue; switch (item.type) { case 'checkbox-group': rawValue = []; item.options.forEach(option => { if (form.elements[`${item.name}[${option.value}]`]?.checked) { rawValue.push(option.value); } }); break; case 'radio-group': rawValue = form.elements[item.name].value; break; case 'checkbox': rawValue = form.elements[item.name].checked; break; default: rawValue = form.elements[item.name].value; } if (item.validValue) { const msg = item.validValue(rawValue); if (msg) { showAlert('warning', msg, 5000); return false; } } const saveValue = item.serializeValue ? item.serializeValue(rawValue) : rawValue; toSaveSettings[item.name] = saveValue; } Object.keys(toSaveSettings).forEach(key => { currentSettings[key] = toSaveSettings[key]; }); GM_setValue(getSettingName(), currentSettings); showAlert('success', '设置已保存', 3000); return true; } // 显示/隐藏菜单 function showMenu() { createSettingsMenu().style.display = 'block'; } function hideMenu() { document.getElementById('tampermonkey-settings-menu')?.remove(); } // 初始化 const handler = chooseActiveHandler(); if (handler == null) { return; } adjustDefaultSetting(handler); initSettings(); GM_registerMenuCommand('脚本设置', showMenu, 's'); if (!currentSettings.enabled) { return; } enhancementByHandler(handler, currentSettings); })();