Greasy Fork is available in English.
用本地时间替换覆盖Stage1论坛中的##时间。
// ==UserScript== // @name Stage1 Local Time Replacer // @name:zh-CN Stage1本地时间替换 // @namespace user-NITOUCHE // @version 1.3.2 // @description Replace and overwrite ##### Standard Time with local time on Stage1 forums. // @description:zh-CN 用本地时间替换覆盖Stage1论坛中的##时间。 // @author DS泥头车 // @match https://*.saraba1st.com/2b/* // @icon https://bbs.saraba1st.com/favicon.ico // @grant GM_addStyle // @license MIT // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 添加 CSS 样式到页面 // 使用 GM_addStyle 是为了避免与页面原有 CSS 冲突,并确保样式能被正确应用 GM_addStyle(` .s1-local-time { font: inherit !important; /* 继承父元素的字体样式,!important 确保覆盖原有样式 */ } .s1-local-time.blue-replaced { color: #000000 !important; /* 替换为蓝色时的时间颜色,!important 确保覆盖原有样式 */ } .s1-local-time.orange-replaced { color: #F26C4F !important; /* 替换为橙色时的时间颜色,!important 确保覆盖原有样式 */ } `); let isProcessing = false; // 标志变量,防止重复处理,避免 MutationObserver 触发多次处理 // 获取元素的颜色值 (RGB 格式) function getElementColor(el) { const color = window.getComputedStyle(el).color; // 获取计算后的颜色值 const rgb = color.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/i); // 使用正则表达式匹配 RGB 格式 if (rgb) { // 将 RGB 转换为十六进制颜色值 (方便比较) return (parseInt(rgb[1]) << 16) | (parseInt(rgb[2]) << 8) | parseInt(rgb[3]); } return null; // 如果颜色格式不是 RGB,则返回 null } // 将北#时间转换为本地时间 function convertBeijingToLocal(beijingTime) { try { // 创建 Date 对象,并指定时区为 UTC+8 (北#时间) const date = new Date(beijingTime + '+08:00'); // 使用 toLocaleString 格式化为本地时间,并指定中文格式 return date.toLocaleString('zh-CN', { year: 'numeric', // 年份:四位数字 month: 'numeric', // 月份:数字 day: 'numeric', // 日期:数字 hour: '2-digit', // 小时:两位数字 (24小时制) minute: '2-digit',// 分钟:两位数字 hour12: false // 禁用 12 小时制 }).replace(/(\d+)\/(\d+)\/(\d+)/, '$1-$2-$3'); // 将日期格式中的斜杠替换为短横线,例如:2023/10/26 -> 2023-10-26 } catch(e) { // 如果转换出错 (例如,时间格式不正确),则返回原始北#时间 return beijingTime; } } // 处理单个元素及其子元素中的时间字符串 function processElement(el) { if (el.dataset.timeReplaced || el.querySelector('[data-time-replaced]')) return; // 如果元素或其子元素已经被处理过,则跳过,避免重复处理 let processed = false; // 标记是否在该元素中找到了并处理了时间 const timeRegex = /(\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2})/; // 匹配 "YYYY-MM-DD HH:MM" 格式的时间字符串的正则表达式 const treeWalker = document.createTreeWalker( el, // 从当前元素开始遍历 NodeFilter.SHOW_TEXT, // 只遍历文本节点 null, // 无需自定义过滤器 false // 不需要实体扩展 ); let textNode; while (textNode = treeWalker.nextNode()) { // 遍历文本节点 if (textNode.textContent.trim() && timeRegex.test(textNode.textContent)) { // 如果文本节点不为空白,并且包含匹配时间格式的字符串 const match = textNode.textContent.match(timeRegex); // 匹配时间字符串 if (!match) continue; // 如果没有匹配到,则继续下一个文本节点 const originalColor = getElementColor(textNode.parentElement); // 获取时间字符串父元素的颜色,用于判断时间颜色类型 let colorClass = ''; // 初始化颜色 CSS 类名 if (originalColor === 0xF26C4F) { // 橙色 (0xF26C4F 是橙色的十六进制 RGB 值) colorClass = 'orange-replaced'; } else if (originalColor === 0x022C80 || originalColor === 0x22c || originalColor === 0x999999) { // 蓝色 (0x022C80, 0x22c, 0x999999 可能是不同深浅的蓝色或灰色) colorClass = 'blue-replaced'; } const timeSpan = document.createElement('span'); // 创建 span 元素用于包裹转换后的本地时间 timeSpan.className = `s1-local-time ${colorClass}`.trim(); // 设置 span 的 class,应用样式 timeSpan.textContent = convertBeijingToLocal(match[0]); // 将北#时间转换为本地时间并设置为 span 的文本内容 timeSpan.dataset.timeReplaced = "true"; // 标记该 span 已经被时间替换过 const beforeTimeText = document.createTextNode(textNode.textContent.substring(0, match.index)); // 创建时间字符串前面的文本节点 const afterTimeText = document.createTextNode(textNode.textContent.substring(match.index + match[0].length)); // 创建时间字符串后面的文本节点 const parentNode = textNode.parentNode; // 获取文本节点的父元素 parentNode.replaceChild(timeSpan, textNode); // 将原来的文本节点替换为 span 元素 if (afterTimeText.textContent) { // 如果时间字符串后面还有文本 timeSpan.parentNode.insertBefore(afterTimeText, timeSpan.nextSibling); // 将后面的文本节点插入到 span 后面 } if (beforeTimeText.textContent) { // 如果时间字符串前面还有文本 timeSpan.parentNode.insertBefore(beforeTimeText, timeSpan); // 将前面的文本节点插入到 span 前面 } processed = true; // 标记在该元素中找到了并处理了时间 break; // 找到并处理一个时间后,跳出当前文本节点的遍历,处理下一个文本节点 (TreeWalker 会自动继续遍历) } } if (processed) { el.dataset.timeReplaced = "true"; // 标记该元素已经被处理过,即使只替换了一个时间,也避免重复处理 } } // 处理页面中所有符合选择器条件的元素 function processAll() { if (isProcessing) return; // 如果正在处理中,则直接返回,避免重复处理 isProcessing = true; // 设置处理中标志 document.querySelectorAll(` em[id^="authorposton"], /* Discuz! 帖子发布时间 */ i.pstatus, /* Discuz! 可能的状态时间 */ cite, /* 引用内容的时间 */ td.by em span, /* Discuz! 回复时间 */ a[href*="forum.php?mod=redirect"], /* Discuz! 跳转链接中的时间 */ div.quote font, /* Discuz! 引用块中的时间 (旧版) */ div.blockquote font, /* Discuz! 引用块中的时间 (新版) */ blockquote font, /* 通用引用块中的时间 */ a[href*="forum.php?mod=misc"], /* Discuz! 其他链接中的时间 */ ul#pbbs li, /* Discuz! 瀑布流帖子列表时间 */ table td, /* 通用表格单元格,可能包含时间 */ span.xg1.xw0, /* Discuz! 一些辅助信息的时间 */ p span, /* 段落中的 span,可能包含时间 */ li.bbda span.xg1 /* Discuz! 列表项辅助信息时间 */ `).forEach(processElement); // 遍历选择器匹配到的所有元素,并调用 processElement 函数进行处理 isProcessing = false; // 清除处理中标志 } processAll(); // 页面加载时立即执行一次,处理页面上已有的时间 // 创建 MutationObserver 监听 DOM 变化 new MutationObserver(mutations => { mutations.forEach(mut => { // 遍历每个 mutation 记录 if (mut.type === 'childList') { // 如果 mutation 类型是子节点列表变化 (即有节点被添加或移除) mut.addedNodes.forEach(node => { // 遍历所有被添加的节点 if (node.nodeType === Node.ELEMENT_NODE) { // 如果添加的节点是元素节点 processAll(); // 重新处理所有符合条件的元素,包括新添加的元素 } }); } }); }).observe(document.body, { // 监听 document.body 及其子树的 DOM 变化 childList: true, // 监听子节点列表变化 (添加或移除节点) subtree: true, // 监听整个子树的变化,包括后代节点 attributes: false // 不监听属性变化 }); })();