From acc075f14b6f50a1b268145358e32c195dba795d Mon Sep 17 00:00:00 2001 From: Longtail Date: Sat, 31 Oct 2026 14:23:39 +0800 Subject: [PATCH] Initial commit --- dww-today-threads.user.js | 133 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 dww-today-threads.user.js diff --git a/dww-today-threads.user.js b/dww-today-threads.user.js new file mode 100644 index 0000000..d88f8a5 --- /dev/null +++ b/dww-today-threads.user.js @@ -0,0 +1,133 @@ +// ==UserScript== +// @name 广谈大湾网 · 今日发帖数 +// @namespace local.dww.today-threads +// @version 1.0.0 +// @description 在论坛主题管理的主题总数右侧显示北京时间当日新增主题数(不含回复)。 +// @match https://www.dww11.com/admin.php* +// @match https://dww11.com/admin.php* +// @run-at document-idle +// @grant none +// ==/UserScript== + +(() => { + 'use strict'; + + // 不要添加 @noframes:管理列表就在后台的 main iframe 内。 + const params = new URLSearchParams(location.search); + if (params.get('action') !== 'threads' || params.get('operation')) return; + + const header = [...document.querySelectorAll('#cpform th.partition')] + .find(el => /^\s*(?:最新主题数|符合条件的主题数)\s*\d+/.test(el.textContent)); + const form = document.querySelector('#threadforum'); + if (!header || !form || document.getElementById('dww-today-threads')) return; + + const badge = document.createElement('span'); + badge.id = 'dww-today-threads'; + badge.style.cssText = 'display:inline-block;margin-left:20px;color:#008640;white-space:nowrap;'; + const label = document.createElement('span'); + label.setAttribute('role', 'status'); + label.setAttribute('aria-live', 'polite'); + label.textContent = '今日发帖数:统计中…'; + const refresh = document.createElement('button'); + refresh.type = 'button'; + refresh.textContent = '刷新'; + refresh.style.cssText = 'margin-left:8px;padding:0;border:0;background:none;color:#2369a0;cursor:pointer;font:inherit;font-weight:normal;'; + badge.append(label, refresh); + header.append(badge); + + function dayRange(now = new Date()) { + // 用 UTC 计算固定东八区日期,避免电脑时区及夏令时影响。 + const offset = 8 * 60 * 60 * 1000; + const localDay = new Date(now.getTime() + offset).toISOString().slice(0, 10); + const previousDay = new Date(Date.parse(localDay + 'T00:00:00Z') - 86400000) + .toISOString().slice(0, 10); + // Discuz 原生查询使用 dateline > starttime 且 <= endtime。 + // 从前一天最后一秒之后开始,完整涵盖今日 00:00:00 至 23:59:59。 + return [localDay, `${previousDay} 23:59:59`, `${localDay} 23:59:59`]; + } + + async function queryCount(start, end) { + const hash = form.querySelector('input[name="formhash"]')?.value; + if (!hash) throw new Error('未找到后台查询凭据,请重新登录后刷新'); + + // 只提交查询字段,不读取/提交 #cpform 的批量修改、删除字段。 + // 不继承当前筛选条件,今日数始终表示全部可查询版块的新增主题。 + const body = new URLSearchParams({ + formhash: hash, searchsubmit: '1', detail: '1', + page: '1', pp: '', perpage: '20', + inforum: 'all', intype: '', insort: 'all', + starttime: start, endtime: end, + users: '', keywords: '', viewsmore: '', viewsless: '', + repliesmore: '', repliesless: '', readpermmore: '', + pricemore: '', noreplydays: '', savethread: '0', hidethread: '0' + }); + const endpoint = new URL('admin.php?action=threads', location.href); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 25000); + try { + const response = await fetch(endpoint.href, { + method: 'POST', credentials: 'same-origin', cache: 'no-store', + body, signal: controller.signal + }); + if (!response.ok) throw new Error(`后台返回 HTTP ${response.status}`); + const buffer = await response.arrayBuffer(); + const charset = response.headers.get('content-type')?.match(/charset=([^;\s]+)/i)?.[1] + || document.characterSet || 'utf-8'; + const html = new TextDecoder(charset).decode(buffer); + const result = new DOMParser().parseFromString(html, 'text/html'); + const summary = [...result.querySelectorAll('#cpform th.partition')] + .map(el => el.textContent.match(/^\s*符合条件的主题数\s*([\d,]+)/)) + .find(Boolean); + if (summary) { + // 必须是按指定日期查询的结果,避免把默认最新主题数误报成今日数。 + const returnedStart = result.querySelector('#threadforum [name="starttime"]')?.value; + const returnedEnd = result.querySelector('#threadforum [name="endtime"]')?.value; + if (returnedStart !== start || returnedEnd !== end) { + throw new Error('后台未返回指定日期条件,无法确认统计结果'); + } + return Number(summary[1].replaceAll(',', '')); + } + throw new Error('未读取到统计结果,可能登录已过期或后台查询异常'); + } finally { + clearTimeout(timer); + } + } + + let busy = false; + let lastDay = ''; + async function update() { + if (busy) return; + busy = true; + refresh.disabled = true; + const [day, start, end] = dayRange(); + label.textContent = '今日发帖数:统计中…'; + badge.style.color = '#008640'; + try { + const count = await queryCount(start, end); + if (dayRange()[0] !== day) { + // 请求恰好跨过午夜,不把昨日结果标为今日。 + label.textContent = '今日发帖数:请刷新'; + lastDay = ''; + return; + } + lastDay = day; + label.textContent = `今日发帖数:${count}`; + badge.title = `北京时间 ${day} 新增主题,不含回复;以后台正常主题查询口径为准。更新于 ${new Date().toLocaleTimeString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false })}`; + } catch (error) { + label.textContent = '今日发帖数:读取失败'; + badge.style.color = '#c0392b'; + badge.title = error.name === 'AbortError' ? '查询超时,请点击刷新重试' : error.message; + } finally { + busy = false; + refresh.disabled = false; + } + } + + refresh.addEventListener('click', update); + // 每分钟一条后台只读查询;后台标签页暂停轮询。 + setInterval(() => { if (!document.hidden) update(); }, 60000); + document.addEventListener('visibilitychange', () => { + if (!document.hidden && lastDay !== dayRange()[0]) update(); + }); + update(); +})();