您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
thz forum helper
当前为
// ==UserScript== // @name thz-helper // @description thz forum helper // @namespace http://tampermonkey.net/ // @version 1.0.1 // @author paso // @match http://96thz.cc/* // @match http://www.example.com/ // @grant none // @license MIT // ==/UserScript== ;(function () { 'use strict' // Your code here... // http://96thz.cc/forum.php?mod=forumdisplay&fid=181&filter=typeid&typeid=36&orderby=heats&page=2 const namespace = 'paso-thz-helper' const __msgType = `${namespace}-cross-origin-storage` const middleware = 'http://www.example.com' if (window.location.origin === middleware) { StorageServer() } else { handleTarget(middleware) } function handleTarget(server) { const CONTEXT = { env: 'prod', dev: { dependency: { jquery: 'http://localhost:3000/test/jquery.slim.min.js', popupInject: 'http://localhost:3000/test/popup-inject.min.js', vue: 'http://localhost:3000/test/vue.global.prod.min.js' } }, prod: { dependency: { jquery: 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/3.6.0/jquery.slim.min.js', popupInject: 'https://fastly.jsdelivr.net/gh/pansong291/[email protected]/src/popup-inject.min.js', vue: 'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/3.2.31/vue.global.prod.min.js' } } } const DEFAULT_DATA = function () { return { executeSelector: '#discuz_tips', path: '/forum.php', params: { fid: '181', filter: 'typeid', typeid: '', orderby: 'heats' }, search: '' } } const storage = StorageClient(server) const querySearch = resolveQuerySearch() const hiddenSelector = { forumdisplay: '.a_fl, .a_fr, #toptb + div[align=center], #diynavtop, #toptb, #hd, #ft, #f_pst, #newspecial, #autopbn', viewthread: '.a_fl, .a_fr, #toptb + div[align=center], #diynavtop, #toptb, #hd, #ft, #f_pst, #newspecial, #pgt, .pgt, .pgbtn, #hiddenpoststip, #postlist > div[id] + div[id], .pgs', index: '.a_fl, .a_fr, #toptb + div[align=center], #diynavtop, #toptb, #hd, #ft, #f_pst, #newspecial, #autopbn, #ct > .mn > style + div, #ct > .mn > div + table' } const POPUP_INJECT_CONFIG = { namespace, actionName: 'Settings', collapse: '70%', location: '35%', content: `<div id="${namespace}-app"></div>`, style: ` <style> .${namespace} * { font-size: 14px; color: black; } .${namespace} .table { display: table; border-collapse: separate; border-spacing: 4px 4px; } .${namespace} .table-row { display: table-row; } .${namespace} .table-cell { display: table-cell; } .${namespace} .text-right { text-align: right; } .${namespace} .flex.gap-8 { gap: 8px; } </style>` } const VUE_COMPONENT_PARAM_SELECT = { template: ` <label class="table-row"> <div class="table-cell text-right">{{title}}</div> <div class="table-cell"> <div class="flex"> <input class="input monospace" v-if="!hideInput" v-model.trim="inputValue" /> <select class="input" v-if="!hideSelect" v-model="inputValue"> <option v-for="o in options" :value="o.value">{{o.label}}</option> </select> </div> </div> </label>`, props: ['title', 'value', 'options', 'hideInput', 'hideSelect'], emits: ['update:value'], computed: { inputValue: { get() { return this.$props.value }, set(v) { this.$emit('update:value', v) } } } } const VUE_APP_CONFIG = { template: ` <div class="flex col gap-8"> <div class="table"> <SelectFormItem title="执行时机" v-model:value="executeSelector" hideSelect="true" /> <SelectFormItem title="path" v-model:value="path" hideSelect="true" /> <SelectFormItem title="板块" v-model:value="params.fid" :options="forms.fidOptions" /> <SelectFormItem title="筛选" v-model:value="params.filter" :options="forms.filterOptions" hideInput="true" /> <SelectFormItem title="系列" v-if="params.filter === 'typeid'" v-model:value="params.typeid" :options="forms.typeidOptions[params.fid]" /> <SelectFormItem title="排序" v-model:value="params.orderby" :options="forms.orderbyOptions" hideInput="true" /> <SelectFormItem title="搜索" v-model:value="search" hideSelect="true" /> </div> <button class="button" @click="apply">应用</button> </div>`, components: { SelectFormItem: VUE_COMPONENT_PARAM_SELECT }, data() { return { ...DEFAULT_DATA(), forms: { fidOptions: [ { value: '181', label: '亚洲無碼原創' }, { value: '220', label: '亚洲有碼原創' }, { value: '182', label: '欧美無碼' }, { value: '69', label: '国内原创(BT)' }, { value: '203', label: '各类合集资源' }, { value: '177', label: '蓝光高清原盘' }, { value: '39', label: '日韩情色(BT)' }, { value: '172', label: '桃花原創合集(BT)' } ], filterOptions: [{ value: 'typeid', label: '系列' }], typeidOptions: { 181: [ { value: '', label: '全部' }, { value: '664', label: 'FC2PPV' }, { value: '33', label: '美女步兵' }, { value: '35', label: '一本道系' }, { value: '36', label: '加勒比系' }, { value: '64', label: '1919go' }, { value: '39', label: '10musu' }, { value: '47', label: '性孽變態' }, { value: '53', label: '素人系列' }, { value: '37', label: '东京热系' }, { value: '38', label: 'HEYZO' }, { value: '116', label: 'MuraTV' }, { value: '67', label: 'HeYPPV' }, { value: '50', label: '仟人斬系' }, { value: '51', label: '金髪天國' }, { value: '52', label: '盜撮系列' }, { value: '49', label: 'ガチん娘' }, { value: '40', label: '人妻熟女' }, { value: '319', label: 'pacoma' }, { value: '195', label: '無毛宣言' }, { value: '194', label: 'メス豚系' }, { value: '226', label: 'RealDiva' }, { value: '114', label: 'XXX-AV' }, { value: '196', label: '誘惑天国' }, { value: '198', label: '经典稀缺' }, { value: '200', label: '問答無用' }, { value: '44', label: 'JavHD' }, { value: '321', label: 'h4610系' }, { value: '322', label: '素人妻系' }, { value: '320', label: '人妻斬系' }, { value: '501', label: 'AV志向系' }, { value: '616', label: '店長推薦' }, { value: '731', label: '本生素人' }, { value: '48', label: '3D影畫' }, { value: '523', label: 'H:G:M:O' }, { value: '222', label: '写真专辑' }, { value: '768', label: '无码流出' }, { value: '770', label: '麻豆传媒' } ], 220: [ { value: '', label: '全部' }, { value: '91', label: '高清騎兵' }, { value: '92', label: '美女騎兵' }, { value: '109', label: '美素人系' }, { value: '110', label: '剧情系列' }, { value: '221', label: '无损原盘' } ], 182: [ { value: '', label: '全部' }, { value: '41', label: 'x-Art' }, { value: '42', label: 'Wow' }, { value: '43', label: 'bangbros' }, { value: '45', label: 'brazzers' }, { value: '120', label: 'naughtyamerica' }, { value: '122', label: 'babes' }, { value: '46', label: 'realitykings' }, { value: '115', label: 'DDF' }, { value: '111', label: '按摩师系' }, { value: '214', label: 'twistys' }, { value: '121', label: 'nubilefilms' }, { value: '197', label: 'hegre-art' }, { value: '227', label: 'wicked' }, { value: '150', label: 'BDSM' }, { value: '216', label: '邪惡天使' }, { value: '219', label: 'vixen' }, { value: '220', label: 'passion-hd' }, { value: '223', label: '18yoga' }, { value: '224', label: 'private' }, { value: '193', label: 'joymii' }, { value: '201', label: '21members' }, { value: '202', label: 'colette' }, { value: '213', label: 'mofos' }, { value: '215', label: 'nubiles' }, { value: '217', label: 'blacked' }, { value: '225', label: 'sexart' }, { value: '199', label: 'Femjoy' }, { value: '228', label: 'digitalplayground' }, { value: '496', label: '18xgirls' }, { value: '497', label: 'teamskeet' }, { value: '498', label: 'sexyhub' }, { value: '499', label: 'fakehub' }, { value: '500', label: 'realitygang' }, { value: '218', label: 'julesjordan' }, { value: '513', label: 'TUSHY' }, { value: '605', label: 'ANALIZED' }, { value: '615', label: 'HARDX' }, { value: '730', label: 'nubiles-porn' }, { value: '732', label: 'lubed' }, { value: '769', label: 'deeper' }, { value: '87', label: 'SM变态' }, { value: '88', label: '其它分类' }, { value: '86', label: '肛交天堂' } ], 69: [ { value: '', label: '全部' }, { value: '9', label: '国内无码' }, { value: '10', label: '国内偷拍' }, { value: '11', label: '主播探花' }, { value: '65', label: '美女资源' }, { value: '192', label: '国模私拍' } ], 203: [ { value: '', label: '全部' }, { value: '55', label: '亚洲无码' }, { value: '56', label: '亚洲有码' }, { value: '57', label: '欧美情色' }, { value: '63', label: '其他资源' } ], 177: [ { value: '', label: '全部' }, { value: '27', label: '亚洲无码' }, { value: '28', label: '亚洲有码' }, { value: '29', label: '欧美情色' }, { value: '30', label: '其他原盘' } ], 39: [ { value: '', label: '全部' }, { value: '1', label: '无码' }, { value: '2', label: '有码' } ], 172: [ { value: '', label: '全部' }, { value: '18', label: '亚洲无码' }, { value: '19', label: '亚洲有码' }, { value: '20', label: '中文字幕' }, { value: '21', label: '欧美情色' }, { value: '22', label: '伦理电影' }, { value: '23', label: '美女写真' }, { value: '24', label: '成人动漫' } ] }, orderbyOptions: [ { value: 'heats', label: '最热' }, { value: 'lastpost', label: '最新' }, { value: 'dateline', label: '时间' } ] } } }, computed: {}, methods: { apply() { storage .setItem(namespace, { executeSelector: this.executeSelector, path: this.path, params: { ...this.params }, search: this.search }) .then(() => (window.location = getPageLocation(this.path, this.params, querySearch.page || '1'))) } }, mounted() { storage .getItem(namespace) .then((resp) => JSON.parse(resp.data)) .catch(() => DEFAULT_DATA()) .then((data) => { this.executeSelector = data.executeSelector this.path = data.path this.params.fid = data.params.fid this.params.filter = data.params.filter this.params.typeid = data.params.typeid this.params.orderby = data.params.orderby this.search = data.search }) } } const dependency = CONTEXT[CONTEXT.env].dependency const window$ = window.$ loadJS(dependency.jquery) .then(() => { window.$ = window$ }) .then(() => loadJS(dependency.popupInject)) .then(() => loadJS(dependency.vue)) .then(() => window.paso.injectPopup(POPUP_INJECT_CONFIG)) .then(() => window.Vue.createApp(VUE_APP_CONFIG).mount(`#${namespace}-app`)) storage .getItem(namespace) .then((resp) => JSON.parse(resp.data)) .catch((e) => { return DEFAULT_DATA() }) .then((data) => ready(data.executeSelector) .then(() => handlePageContent(data)) .catch((e) => { console.warn(e) }) ) function handlePageContent(data) { // 隐藏广告 const list = document.querySelectorAll(hiddenSelector[querySearch.mod || 'index']) list?.forEach((el) => { el.setAttribute('style', 'display: none !important;') }) // 替换分页 const { path, params, search } = data document.querySelectorAll('#fd_page_bottom > .pg, #fd_page_top > .pg')?.forEach((pw) => { const strong = pw.querySelector('strong') let currentPage = 1 if (strong) { currentPage = getStartInt(strong.innerText) } pw.querySelectorAll('a[href]')?.forEach((page) => { const pageNum = getEndInt(page.innerText) if (isNaN(pageNum)) { if (page.classList.contains('prev')) { page.href = getPageLocation(path, params, currentPage - 1) } else if (page.classList.contains('nxt')) { page.href = getPageLocation(path, params, currentPage + 1) } } else { page.href = getPageLocation(path, params, pageNum) } }) const pageInput = pw.querySelector('input[name=custompage]') if (pageInput) { pageInput.onkeydown = function (event) { if (event.keyCode === 13) { window.location = getPageLocation(path, params, this.value) window.doane?.(event) } } } }) // 过滤结果 const notMatch = [] const match = (t) => { return t && t.indexOf && t.indexOf(search) >= 0 } document.querySelectorAll('#threadlisttableid > tbody')?.forEach((item) => { if (!match(item.querySelector('a.s.xst')?.innerText)) { notMatch.push(item) } }) const setFilter = (filter) => { notMatch.forEach((item) => { if (filter) { item.setAttribute('style', 'display: none !important;') } else { item.removeAttribute('style') } }) } // 增加过滤按钮 const tf = document.querySelector('#threadlist .tf') if (tf) { let filterCb = tf.querySelector(`label input.${namespace}`) if (!filterCb) { filterCb = document.createElement('input') filterCb.classList.add(namespace) filterCb.type = 'checkbox' const label = document.createElement('label') label.append(filterCb, document.createTextNode('只看搜索结果')) tf.append(document.createTextNode('\xA0'), label) } filterCb.checked = !!search if (search) setFilter(true) filterCb.onchange = (e) => { setFilter(e.target.checked) } } } if (CONTEXT.env === 'dev') { window._$_getTypes = function () { const arr = [] document.querySelectorAll('ul#thread_types > li > a')?.forEach((a) => { const item = { value: '' } if (a.firstChild && a.firstChild instanceof Text) { item.label = a.firstChild.wholeText } if (a.href) { const i = a.href.indexOf('?') if (i >= 0) { const qs = resolveQuerySearch(a.href.substring(i)) item.value = qs.typeid || '' } } arr.push(item) }) console.log(arr) console.log(JSON.stringify(arr)) } } } function resolveQuerySearch(search) { const result = {} search = search || window.location.search if (search) { if (search.startsWith('?')) { search = search.substring(1) } search .split('&') .map((entry) => { return entry.split('=') }) .forEach((entry) => { result[entry[0]] = entry[1] }) } return result } function loadJS(src) { return new Promise((resolve) => { const script = document.createElement('script') script.src = src script.onload = resolve document.head.append(script) }) } function ready(selector, interval = 300, timeout = 3000) { return new Promise((resolve, reject) => { const loopId = setInterval( (startTime) => { if (document.querySelector(selector)) { clearInterval(loopId) resolve() } else { if (Date.now() - startTime > timeout) { clearInterval(loopId) reject(`look up for target '${selector}' timeout: ${timeout}ms`) } } }, interval, Date.now() ) }) } function getStartInt(str) { let result = '' if (str) { for (let i = 0; i < str.length; i++) { if (isNaN(parseInt(str[i]))) { if (result) break } else { result += str[i] } } } return parseInt(result) } function getEndInt(str) { let result = '' if (str) { for (let i = str.length - 1; i >= 0; i--) { if (isNaN(parseInt(str[i]))) { if (result) break } else { result = str[i] + result } } } return parseInt(result) } function getPageLocation(path, p, num) { const params = { mod: 'forumdisplay', fid: p.fid || '', filter: p.filter || '', typeid: p.typeid || '', orderby: p.orderby || '', page: num } const paramStr = '?' + Object.entries(params) .map((entry) => { return `${entry[0]}=${entry[1]}` }) .join('&') return path + paramStr } /** * 生成随机ID */ function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } function StorageClient(middlewareUrl) { const _requests = {} //所有请求消息数据映射 const _cache = { ready: false, queue: [] } //获取 iframe window 对象 const _iframeWin = _createIframe(middlewareUrl).contentWindow _initListener() //监听 /** * 创建 iframe 标签 * @param {string} middlewareUrl * @return Object */ function _createIframe(middlewareUrl) { const iframe = document.createElement('iframe') iframe.src = middlewareUrl iframe.setAttribute('style', 'display: none !important;') window.document.body.appendChild(iframe) return iframe } /** * 初始化监听函数 */ function _initListener() { // 监听 iframe “中转页面”返回的消息 window.addEventListener('message', (e) => { if (e?.data?.__msgType !== __msgType) return if (e.data.ready) { _cache.ready = true while (_cache.queue.length) { _iframeWin.postMessage(_cache.queue.shift(), '*') } return } let { id, response } = e.data // 找到“中转页面”的消息对应的回调函数 let currentCallback = _requests[id] if (!currentCallback) return // 调用并返回数据 currentCallback(response, e.data) delete _requests[id] }) } /** * 发起请求函数 * @param method 请求方式 * @param key * @param value */ function _requestFn(method, key, value) { return new Promise((resolve) => { // 发消息时,请求对象格式 const req = { id: uuid(), method, key, value, __msgType } //请求唯一标识 id 和回调函数的映射 _requests[req.id] = resolve if (_cache.ready) { _iframeWin.postMessage(req, '*') } else { _cache.queue.push(req) } }) } return { /** * 获取存储数据 * @param {Object | string} key */ getItem(key) { return _requestFn('get', key) }, /** * 更新存储数据 * @param {Object | string} key * @param {Object | string} value */ setItem(key, value) { return _requestFn('set', key, value) }, /** * 删除数据 * @param {Object | string} key */ delItem(key) { return _requestFn('delete', key) }, /** * 清除数据 */ clear() { return _requestFn('clear') } } } function StorageServer() { if (window.parent === window) return const functionMap = { /** * 设置数据 * @param {Object | string} key * @param {?Object | ?string} value */ setStore(key, value) { if (!key) return if (typeof key === 'string') { return localStorage.setItem(key, typeof value === 'object' ? JSON.stringify(value) : value) } Object.keys(key).forEach((dataKey) => { let dataValue = typeof key[dataKey] === 'object' ? JSON.stringify(key[dataKey]) : key[dataKey] localStorage.setItem(dataKey, dataValue) }) }, /** * 获取数据 * @param {Object | string} key */ getStore(key) { if (!key) return if (typeof key === 'string') return localStorage.getItem(key) let dataRes = {} Object.keys(key).forEach((dataKey) => { dataRes[dataKey] = localStorage.getItem(dataKey) || null }) return dataRes }, /** * 删除数据 * @param {Object | string} key */ deleteStore(key) { if (!key) return if (typeof key === 'string') return localStorage.removeItem(key) Object.keys(key).forEach((dataKey) => { localStorage.removeItem(dataKey) }) }, /** * 清空 */ clearStore() { localStorage.clear() } } _initListener() //监听消息 // 通知父页面 window.parent.postMessage( { ready: true, __msgType }, '*' ) /** * 监听 */ function _initListener() { window.addEventListener('message', (e) => { if (e?.data?.__msgType !== __msgType) return const { method, key, value, id = 'default' } = e.data //获取方法 const func = functionMap[`${method}Store`] //取出本地的数据 const response = { data: func?.(key, value) } if (!func) response.errorMsg = 'Request method error!' //发送给父页面 window.parent.postMessage( { id, request: e.data, response, __msgType }, '*' ) }) } } })()