“MediaWiki:Common.js”的版本间差异
跳转到导航
跳转到搜索
(将特效更名为鼠标特效,并简化为开启/关闭两个选项) |
|||
| (未显示2个用户的13个中间版本) | |||
| 第1行: | 第1行: | ||
/* 这里的任何JavaScript将为所有用户在每次页面载入时加载。 */ | /* 这里的任何JavaScript将为所有用户在每次页面载入时加载。 */ | ||
| − | // | + | /* 阅读主题:仅在当前浏览器保存,未选择时跟随系统。 */ |
| + | (function () { | ||
| + | 'use strict'; | ||
| + | |||
| + | var root = document.documentElement; | ||
| + | var storageKey = 'rmzas-wiki-theme'; | ||
| + | var systemTheme = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; | ||
| + | var selector; | ||
| + | var mode = 'auto'; | ||
| + | |||
| + | // 防止脚本重复执行时添加多个控件或事件监听。 | ||
| + | if (root.hasAttribute('data-rmzas-theme')) return; | ||
| + | |||
| + | function normalize(value) { | ||
| + | return value === 'light' || value === 'dark' ? value : 'auto'; | ||
| + | } | ||
| + | |||
| + | function applyTheme() { | ||
| + | var dark = mode === 'dark' || (mode === 'auto' && systemTheme && systemTheme.matches); | ||
| + | root.setAttribute('data-rmzas-theme', mode); | ||
| + | root.classList.toggle('rmzas-theme-dark', !!dark); | ||
| + | if (selector) selector.value = mode; | ||
| + | } | ||
| + | |||
| + | try { | ||
| + | mode = normalize(window.localStorage.getItem(storageKey)); | ||
| + | } catch (error) { | ||
| + | // 存储不可用时仍允许切换,当前页面内保留选择。 | ||
| + | } | ||
| + | // 不等待 DOM 就绪,尽早应用配色;Common.js 本身仍是异步加载的。 | ||
| + | applyTheme(); | ||
| + | |||
| + | if (systemTheme) { | ||
| + | if (systemTheme.addEventListener) { | ||
| + | systemTheme.addEventListener('change', applyTheme); | ||
| + | } else if (systemTheme.addListener) { | ||
| + | systemTheme.addListener(applyTheme); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | window.addEventListener('storage', function (event) { | ||
| + | if (event.key === storageKey || event.key === null) { | ||
| + | mode = normalize(event.newValue); | ||
| + | applyTheme(); | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | function addThemeControl() { | ||
| + | if (document.getElementById('rmzas-theme-select')) return; | ||
| + | |||
| + | // Vector / MonoBook 的个人工具栏;Timeless 使用用户链接区域。 | ||
| + | var menu = document.querySelector('#p-personal ul, #p-personal .mw-portlet-body ul, #p-personal .body ul, #p-personal-inner ul'); | ||
| + | var control = document.createElement(menu ? 'li' : 'div'); | ||
| + | control.id = 'pt-rmzas-theme'; | ||
| + | control.className = 'rmzas-theme-control'; | ||
| + | if (!menu) control.classList.add('rmzas-theme-control-floating'); | ||
| + | |||
| + | var label = document.createElement('label'); | ||
| + | label.htmlFor = 'rmzas-theme-select'; | ||
| + | label.textContent = '主题'; | ||
| + | selector = document.createElement('select'); | ||
| + | selector.id = 'rmzas-theme-select'; | ||
| + | selector.title = '选择阅读主题(此浏览器会记住你的选择)'; | ||
| + | |||
| + | [['auto', '跟随系统'], ['light', '浅色'], ['dark', '深色']].forEach(function (item) { | ||
| + | var option = document.createElement('option'); | ||
| + | option.value = item[0]; | ||
| + | option.textContent = item[1]; | ||
| + | selector.appendChild(option); | ||
| + | }); | ||
| + | selector.value = mode; | ||
| + | selector.addEventListener('change', function () { | ||
| + | mode = normalize(selector.value); | ||
| + | applyTheme(); | ||
| + | try { | ||
| + | window.localStorage.setItem(storageKey, mode); | ||
| + | } catch (error) { | ||
| + | // 隐私设置禁用存储时不影响配色或原有站点脚本。 | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | control.appendChild(label); | ||
| + | control.appendChild(selector); | ||
| + | if (menu) { | ||
| + | menu.insertBefore(control, menu.firstChild); | ||
| + | } else { | ||
| + | document.body.appendChild(control); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | if (document.readyState === 'loading') { | ||
| + | document.addEventListener('DOMContentLoaded', addThemeControl); | ||
| + | } else { | ||
| + | addThemeControl(); | ||
| + | } | ||
| + | })(); | ||
| + | |||
| + | /* BA鼠标光标与点击特效:支持开启/关闭,仅在当前浏览器保存。 */ | ||
| + | (function () { | ||
| + | 'use strict'; | ||
| + | |||
| + | var root = document.documentElement; | ||
| + | var storageKey = 'rmzas-wiki-fx'; | ||
| + | var scriptUrl = 'http://rmzas.site/wp-content/themes/twentyfifteen/js/ba-click-fx/ba-click-fx.js'; | ||
| + | var motionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; | ||
| + | var BAClickFXClass = null; | ||
| + | var activeInstances = new Set(); | ||
| + | var isPatching = false; | ||
| + | var selector; | ||
| + | var mode = 'on'; // 'on' | 'off' | ||
| + | |||
| + | if (root.hasAttribute('data-rmzas-fx')) return; | ||
| + | |||
| + | function normalize(value) { | ||
| + | if (value === 'off') return 'off'; | ||
| + | if (value === 'on') return 'on'; | ||
| + | // 首次访问未设置时:若操作系统开启了减弱动态效果则默认关闭,否则默认开启 | ||
| + | return (motionQuery && motionQuery.matches) ? 'off' : 'on'; | ||
| + | } | ||
| + | |||
| + | function shouldEnable() { | ||
| + | return mode === 'on'; | ||
| + | } | ||
| + | |||
| + | // 确保页面中有 #myCanvas 供 BAClickFX 挂载,防止其回退生成独立的顶层 canvas | ||
| + | function ensureTargetCanvas() { | ||
| + | var cvs = document.getElementById('myCanvas'); | ||
| + | if (!cvs) { | ||
| + | cvs = document.createElement('canvas'); | ||
| + | cvs.id = 'myCanvas'; | ||
| + | if (document.body) { | ||
| + | document.body.appendChild(cvs); | ||
| + | } else { | ||
| + | document.addEventListener('DOMContentLoaded', function () { | ||
| + | if (!document.getElementById('myCanvas') && document.body) { | ||
| + | document.body.appendChild(cvs); | ||
| + | } | ||
| + | }); | ||
| + | } | ||
| + | } | ||
| + | return cvs; | ||
| + | } | ||
| + | |||
| + | function hideAllFxCanvases() { | ||
| + | var canvases = document.querySelectorAll('canvas'); | ||
| + | for (var i = 0; i < canvases.length; i++) { | ||
| + | canvases[i].style.setProperty('display', 'none', 'important'); | ||
| + | canvases[i].style.setProperty('visibility', 'hidden', 'important'); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function showAllFxCanvases() { | ||
| + | var canvases = document.querySelectorAll('canvas'); | ||
| + | for (var i = 0; i < canvases.length; i++) { | ||
| + | canvases[i].style.removeProperty('display'); | ||
| + | canvases[i].style.removeProperty('visibility'); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function pauseAll() { | ||
| + | activeInstances.forEach(function (inst) { | ||
| + | try { | ||
| + | if (typeof inst.setPaused === 'function') { | ||
| + | inst.setPaused(true); | ||
| + | } | ||
| + | if (typeof inst.clear === 'function') { | ||
| + | inst.clear(); | ||
| + | } | ||
| + | } catch (e) { | ||
| + | console.warn('[BAClickFX] pause error:', e); | ||
| + | } | ||
| + | }); | ||
| + | hideAllFxCanvases(); | ||
| + | } | ||
| + | |||
| + | function resumeAll() { | ||
| + | showAllFxCanvases(); | ||
| + | activeInstances.forEach(function (inst) { | ||
| + | try { | ||
| + | if (typeof inst.setPaused === 'function') { | ||
| + | inst.setPaused(false); | ||
| + | } | ||
| + | } catch (e) { | ||
| + | console.warn('[BAClickFX] resume error:', e); | ||
| + | } | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | // 劫持 BAClickFX 类原型:拦截所有实例的事件分发与渲染 | ||
| + | function patchBAClickFXClass(Cls) { | ||
| + | if (!Cls || !Cls.prototype || Cls.__rmzasPatched) return; | ||
| + | Cls.__rmzasPatched = true; | ||
| + | BAClickFXClass = Cls; | ||
| + | |||
| + | var origPointerMove = Cls.prototype._handlePointerMove; | ||
| + | if (typeof origPointerMove === 'function') { | ||
| + | Cls.prototype._handlePointerMove = function (e) { | ||
| + | if (!shouldEnable()) return; | ||
| + | return origPointerMove.apply(this, arguments); | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | var origPointerDown = Cls.prototype._handlePointerDown; | ||
| + | if (typeof origPointerDown === 'function') { | ||
| + | Cls.prototype._handlePointerDown = function (e) { | ||
| + | if (!shouldEnable()) return; | ||
| + | return origPointerDown.apply(this, arguments); | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | var origRenderFrame = Cls.prototype._renderFrame; | ||
| + | if (typeof origRenderFrame === 'function') { | ||
| + | Cls.prototype._renderFrame = function (e) { | ||
| + | if (!shouldEnable()) { | ||
| + | this.animationFrame = null; | ||
| + | return; | ||
| + | } | ||
| + | return origRenderFrame.apply(this, arguments); | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | var origMount = Cls.prototype._applyCompositingMount; | ||
| + | if (typeof origMount === 'function') { | ||
| + | Cls.prototype._applyCompositingMount = function () { | ||
| + | activeInstances.add(this); | ||
| + | var res = origMount.apply(this, arguments); | ||
| + | if (!shouldEnable()) { | ||
| + | try { | ||
| + | this.setPaused(true); | ||
| + | this.clear(); | ||
| + | } catch (err) {} | ||
| + | } | ||
| + | return res; | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | var origDestroy = Cls.prototype.destroy; | ||
| + | if (typeof origDestroy === 'function') { | ||
| + | Cls.prototype.destroy = function () { | ||
| + | activeInstances.delete(this); | ||
| + | return origDestroy.apply(this, arguments); | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | if (!shouldEnable()) { | ||
| + | pauseAll(); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function ensureScriptLoadedAndPatched(callback) { | ||
| + | if (BAClickFXClass) { | ||
| + | if (callback) callback(BAClickFXClass); | ||
| + | return; | ||
| + | } | ||
| + | if (window.BAClickFX) { | ||
| + | patchBAClickFXClass(window.BAClickFX); | ||
| + | if (callback) callback(window.BAClickFX); | ||
| + | return; | ||
| + | } | ||
| + | if (isPatching) return; | ||
| + | isPatching = true; | ||
| + | try { | ||
| + | import(scriptUrl) | ||
| + | .then(function (module) { | ||
| + | isPatching = false; | ||
| + | var Cls = module.BAClickFX || module.default; | ||
| + | if (Cls) { | ||
| + | window.BAClickFX = Cls; | ||
| + | patchBAClickFXClass(Cls); | ||
| + | if (callback) callback(Cls); | ||
| + | } | ||
| + | }) | ||
| + | .catch(function (error) { | ||
| + | isPatching = false; | ||
| + | console.error('[BAClickFX] 加载失败:', error); | ||
| + | }); | ||
| + | } catch (error) { | ||
| + | isPatching = false; | ||
| + | console.error('[BAClickFX] 动态 import 不可用:', error); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function applyFx() { | ||
| + | var enabled = shouldEnable(); | ||
| + | root.setAttribute('data-rmzas-fx', mode); | ||
| + | root.classList.toggle('rmzas-fx-disabled', !enabled); | ||
| + | if (selector) selector.value = mode; | ||
| + | |||
| + | if (!enabled) { | ||
| + | pauseAll(); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | resumeAll(); | ||
| + | |||
| + | // 如果还没有任何实例运行,则确保创建并挂载一个 | ||
| + | if (activeInstances.size === 0) { | ||
| + | ensureScriptLoadedAndPatched(function (Cls) { | ||
| + | if (!shouldEnable() || activeInstances.size > 0) return; | ||
| + | try { | ||
| + | ensureTargetCanvas(); | ||
| + | var instance = new Cls({ | ||
| + | target: '#myCanvas', | ||
| + | trailAlways: true, | ||
| + | trailEnabled: true | ||
| + | }); | ||
| + | activeInstances.add(instance); | ||
| + | } catch (error) { | ||
| + | console.error('[BAClickFX] 初始化失败:', error); | ||
| + | } | ||
| + | }); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | try { | ||
| + | mode = normalize(window.localStorage.getItem(storageKey)); | ||
| + | } catch (error) { | ||
| + | // 存储不可用时当前页面内保留选择。 | ||
| + | } | ||
| + | |||
| + | // 保证页面尽早有目标 Canvas | ||
| + | ensureTargetCanvas(); | ||
| + | |||
| + | // 启动劫持逻辑 | ||
| + | ensureScriptLoadedAndPatched(); | ||
| + | |||
| + | // 尽早应用状态 | ||
| + | applyFx(); | ||
| + | |||
| + | // 暴露全局接口 | ||
| + | window.__rmzasGetFxState = shouldEnable; | ||
| + | window.__rmzasRegisterBAClickFX = function (instance) { | ||
| + | activeInstances.add(instance); | ||
| + | if (!shouldEnable()) { | ||
| + | pauseAll(); | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | window.addEventListener('storage', function (event) { | ||
| + | if (event.key === storageKey || event.key === null) { | ||
| + | mode = normalize(event.newValue); | ||
| + | applyFx(); | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | function addFxControl() { | ||
| + | if (document.getElementById('rmzas-fx-select')) return; | ||
| + | |||
| + | var menu = document.querySelector('#p-personal ul, #p-personal .mw-portlet-body ul, #p-personal .body ul, #p-personal-inner ul'); | ||
| + | var control = document.createElement(menu ? 'li' : 'div'); | ||
| + | control.id = 'pt-rmzas-fx'; | ||
| + | control.className = 'rmzas-fx-control'; | ||
| + | if (!menu) control.classList.add('rmzas-fx-control-floating'); | ||
| + | |||
| + | var label = document.createElement('label'); | ||
| + | label.htmlFor = 'rmzas-fx-select'; | ||
| + | label.textContent = '鼠标特效'; | ||
| + | selector = document.createElement('select'); | ||
| + | selector.id = 'rmzas-fx-select'; | ||
| + | selector.title = '选择是否开启鼠标光标与点击特效(此浏览器会记住你的选择)'; | ||
| + | |||
| + | [['on', '开启'], ['off', '关闭']].forEach(function (item) { | ||
| + | var option = document.createElement('option'); | ||
| + | option.value = item[0]; | ||
| + | option.textContent = item[1]; | ||
| + | selector.appendChild(option); | ||
| + | }); | ||
| + | selector.value = mode; | ||
| + | selector.addEventListener('change', function () { | ||
| + | mode = normalize(selector.value); | ||
| + | applyFx(); | ||
| + | try { | ||
| + | window.localStorage.setItem(storageKey, mode); | ||
| + | } catch (error) {} | ||
| + | }); | ||
| + | |||
| + | control.appendChild(label); | ||
| + | control.appendChild(selector); | ||
| + | |||
| + | var themeControl = document.getElementById('pt-rmzas-theme'); | ||
| + | if (themeControl && themeControl.nextSibling) { | ||
| + | themeControl.parentNode.insertBefore(control, themeControl.nextSibling); | ||
| + | } else if (themeControl && themeControl.parentNode) { | ||
| + | themeControl.parentNode.appendChild(control); | ||
| + | } else if (menu) { | ||
| + | menu.appendChild(control); | ||
| + | } else { | ||
| + | document.body.appendChild(control); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | if (document.readyState === 'loading') { | ||
| + | document.addEventListener('DOMContentLoaded', addFxControl); | ||
| + | } else { | ||
| + | addFxControl(); | ||
| + | } | ||
| + | })(); | ||
| + | |||
| + | // 显示最近更新的页面列表(带时间) | ||
setTimeout(function() { | setTimeout(function() { | ||
| − | console.log(' | + | //console.log('开始加载最近更新页面列表'); |
| − | |||
var sidebar = document.getElementById('mw-panel'); | var sidebar = document.getElementById('mw-panel'); | ||
if (!sidebar) { | if (!sidebar) { | ||
| − | console.log(' | + | console.log('未找到侧边栏'); |
return; | return; | ||
} | } | ||
| − | |||
var items = sidebar.getElementsByTagName('li'); | var items = sidebar.getElementsByTagName('li'); | ||
| − | + | var targetItem = null; | |
| − | |||
| − | |||
for (var i = 0; i < items.length; i++) { | for (var i = 0; i < items.length; i++) { | ||
| − | + | if (items[i].textContent.indexOf('最近更新') !== -1) { | |
| − | + | targetItem = items[i]; | |
| − | + | break; | |
| − | |||
| − | |||
| − | items[i] | ||
| − | |||
} | } | ||
} | } | ||
| − | // | + | if (!targetItem) { |
| − | + | console.log('未找到目标容器'); | |
| − | }, | + | return; |
| + | } | ||
| + | |||
| + | targetItem.textContent = ''; | ||
| + | targetItem.innerHTML = '最近更新:<br>加载中...'; | ||
| + | |||
| + | // API请求添加timestamp参数,获取更新时间 | ||
| + | var apiUrl = mw.util.wikiScript('api') + '?'; | ||
| + | apiUrl += 'action=query'; | ||
| + | apiUrl += '&list=recentchanges'; | ||
| + | apiUrl += '&rcprop=title|timestamp'; // 同时获取标题和时间戳 | ||
| + | apiUrl += '&rclimit=10'; | ||
| + | apiUrl += '&rcdaylimit=30'; | ||
| + | apiUrl += '&format=json'; | ||
| + | |||
| + | fetch(apiUrl) | ||
| + | .then(function(response) { | ||
| + | if (!response.ok) throw new Error('API请求失败:' + response.status); | ||
| + | return response.json(); | ||
| + | }) | ||
| + | .then(function(data) { | ||
| + | targetItem.innerHTML = '最近更新:<br>'; | ||
| + | |||
| + | if (!data.query || !data.query.recentchanges || data.query.recentchanges.length === 0) { | ||
| + | targetItem.innerHTML += '暂无更新'; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | var seenTitles = new Set(); | ||
| + | var uniqueUpdates = []; | ||
| + | for (var i = 0; i < data.query.recentchanges.length; i++) { | ||
| + | var page = data.query.recentchanges[i]; | ||
| + | var title = page.title; | ||
| + | if (!seenTitles.has(title)) { | ||
| + | seenTitles.add(title); | ||
| + | uniqueUpdates.push(page); // 包含title和timestamp | ||
| + | if (uniqueUpdates.length >= 6) break; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | var list = document.createElement('ul'); | ||
| + | list.style.listStyle = 'none'; | ||
| + | list.style.paddingLeft = '10px'; | ||
| + | list.style.margin = '5px 0'; | ||
| + | |||
| + | uniqueUpdates.forEach(function(page) { | ||
| + | // 处理时间戳:转换为本地时间(年-月-日 时:分) | ||
| + | var date = new Date(page.timestamp); | ||
| + | var timeStr = date.toLocaleString('zh-CN', { | ||
| + | month: '2-digit', // 月份(2位) | ||
| + | day: '2-digit', // 日期(2位) | ||
| + | hour: '2-digit', // 小时(2位) | ||
| + | minute: '2-digit', // 分钟(2位) | ||
| + | second: '2-digit', // 新增:显示秒(2位) | ||
| + | hour12: false // 强制24小时制(避免上午/下午) | ||
| + | }).replace(',', ''); // 移除可能的逗号分隔符 | ||
| + | |||
| + | var listItem = document.createElement('li'); | ||
| + | listItem.style.margin = '3px 0'; // 每条间距 | ||
| + | |||
| + | var link = document.createElement('a'); | ||
| + | link.href = mw.util.getUrl(page.title); | ||
| + | link.textContent = page.title; | ||
| + | |||
| + | // 时间显示(灰色小字) | ||
| + | var timeSpan = document.createElement('span'); | ||
| + | timeSpan.textContent = ' (' + timeStr + ')'; | ||
| + | timeSpan.className = 'rmzas-recent-time'; | ||
| + | timeSpan.style.fontSize = '0.8em'; // 小字 | ||
| + | timeSpan.style.marginLeft = '5px'; | ||
| + | |||
| + | listItem.appendChild(link); | ||
| + | listItem.appendChild(timeSpan); | ||
| + | list.appendChild(listItem); | ||
| + | }); | ||
| + | |||
| + | targetItem.appendChild(list); | ||
| + | }) | ||
| + | .catch(function(error) { | ||
| + | console.error('加载失败:', error); | ||
| + | targetItem.innerHTML = '最近更新:<br>加载失败'; | ||
| + | }); | ||
| + | |||
| + | }, 1000); | ||
2026年10月22日 (四) 02:33:24的最新版本
/* 这里的任何JavaScript将为所有用户在每次页面载入时加载。 */
/* 阅读主题:仅在当前浏览器保存,未选择时跟随系统。 */
(function () {
'use strict';
var root = document.documentElement;
var storageKey = 'rmzas-wiki-theme';
var systemTheme = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
var selector;
var mode = 'auto';
// 防止脚本重复执行时添加多个控件或事件监听。
if (root.hasAttribute('data-rmzas-theme')) return;
function normalize(value) {
return value === 'light' || value === 'dark' ? value : 'auto';
}
function applyTheme() {
var dark = mode === 'dark' || (mode === 'auto' && systemTheme && systemTheme.matches);
root.setAttribute('data-rmzas-theme', mode);
root.classList.toggle('rmzas-theme-dark', !!dark);
if (selector) selector.value = mode;
}
try {
mode = normalize(window.localStorage.getItem(storageKey));
} catch (error) {
// 存储不可用时仍允许切换,当前页面内保留选择。
}
// 不等待 DOM 就绪,尽早应用配色;Common.js 本身仍是异步加载的。
applyTheme();
if (systemTheme) {
if (systemTheme.addEventListener) {
systemTheme.addEventListener('change', applyTheme);
} else if (systemTheme.addListener) {
systemTheme.addListener(applyTheme);
}
}
window.addEventListener('storage', function (event) {
if (event.key === storageKey || event.key === null) {
mode = normalize(event.newValue);
applyTheme();
}
});
function addThemeControl() {
if (document.getElementById('rmzas-theme-select')) return;
// Vector / MonoBook 的个人工具栏;Timeless 使用用户链接区域。
var menu = document.querySelector('#p-personal ul, #p-personal .mw-portlet-body ul, #p-personal .body ul, #p-personal-inner ul');
var control = document.createElement(menu ? 'li' : 'div');
control.id = 'pt-rmzas-theme';
control.className = 'rmzas-theme-control';
if (!menu) control.classList.add('rmzas-theme-control-floating');
var label = document.createElement('label');
label.htmlFor = 'rmzas-theme-select';
label.textContent = '主题';
selector = document.createElement('select');
selector.id = 'rmzas-theme-select';
selector.title = '选择阅读主题(此浏览器会记住你的选择)';
[['auto', '跟随系统'], ['light', '浅色'], ['dark', '深色']].forEach(function (item) {
var option = document.createElement('option');
option.value = item[0];
option.textContent = item[1];
selector.appendChild(option);
});
selector.value = mode;
selector.addEventListener('change', function () {
mode = normalize(selector.value);
applyTheme();
try {
window.localStorage.setItem(storageKey, mode);
} catch (error) {
// 隐私设置禁用存储时不影响配色或原有站点脚本。
}
});
control.appendChild(label);
control.appendChild(selector);
if (menu) {
menu.insertBefore(control, menu.firstChild);
} else {
document.body.appendChild(control);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addThemeControl);
} else {
addThemeControl();
}
})();
/* BA鼠标光标与点击特效:支持开启/关闭,仅在当前浏览器保存。 */
(function () {
'use strict';
var root = document.documentElement;
var storageKey = 'rmzas-wiki-fx';
var scriptUrl = 'http://rmzas.site/wp-content/themes/twentyfifteen/js/ba-click-fx/ba-click-fx.js';
var motionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
var BAClickFXClass = null;
var activeInstances = new Set();
var isPatching = false;
var selector;
var mode = 'on'; // 'on' | 'off'
if (root.hasAttribute('data-rmzas-fx')) return;
function normalize(value) {
if (value === 'off') return 'off';
if (value === 'on') return 'on';
// 首次访问未设置时:若操作系统开启了减弱动态效果则默认关闭,否则默认开启
return (motionQuery && motionQuery.matches) ? 'off' : 'on';
}
function shouldEnable() {
return mode === 'on';
}
// 确保页面中有 #myCanvas 供 BAClickFX 挂载,防止其回退生成独立的顶层 canvas
function ensureTargetCanvas() {
var cvs = document.getElementById('myCanvas');
if (!cvs) {
cvs = document.createElement('canvas');
cvs.id = 'myCanvas';
if (document.body) {
document.body.appendChild(cvs);
} else {
document.addEventListener('DOMContentLoaded', function () {
if (!document.getElementById('myCanvas') && document.body) {
document.body.appendChild(cvs);
}
});
}
}
return cvs;
}
function hideAllFxCanvases() {
var canvases = document.querySelectorAll('canvas');
for (var i = 0; i < canvases.length; i++) {
canvases[i].style.setProperty('display', 'none', 'important');
canvases[i].style.setProperty('visibility', 'hidden', 'important');
}
}
function showAllFxCanvases() {
var canvases = document.querySelectorAll('canvas');
for (var i = 0; i < canvases.length; i++) {
canvases[i].style.removeProperty('display');
canvases[i].style.removeProperty('visibility');
}
}
function pauseAll() {
activeInstances.forEach(function (inst) {
try {
if (typeof inst.setPaused === 'function') {
inst.setPaused(true);
}
if (typeof inst.clear === 'function') {
inst.clear();
}
} catch (e) {
console.warn('[BAClickFX] pause error:', e);
}
});
hideAllFxCanvases();
}
function resumeAll() {
showAllFxCanvases();
activeInstances.forEach(function (inst) {
try {
if (typeof inst.setPaused === 'function') {
inst.setPaused(false);
}
} catch (e) {
console.warn('[BAClickFX] resume error:', e);
}
});
}
// 劫持 BAClickFX 类原型:拦截所有实例的事件分发与渲染
function patchBAClickFXClass(Cls) {
if (!Cls || !Cls.prototype || Cls.__rmzasPatched) return;
Cls.__rmzasPatched = true;
BAClickFXClass = Cls;
var origPointerMove = Cls.prototype._handlePointerMove;
if (typeof origPointerMove === 'function') {
Cls.prototype._handlePointerMove = function (e) {
if (!shouldEnable()) return;
return origPointerMove.apply(this, arguments);
};
}
var origPointerDown = Cls.prototype._handlePointerDown;
if (typeof origPointerDown === 'function') {
Cls.prototype._handlePointerDown = function (e) {
if (!shouldEnable()) return;
return origPointerDown.apply(this, arguments);
};
}
var origRenderFrame = Cls.prototype._renderFrame;
if (typeof origRenderFrame === 'function') {
Cls.prototype._renderFrame = function (e) {
if (!shouldEnable()) {
this.animationFrame = null;
return;
}
return origRenderFrame.apply(this, arguments);
};
}
var origMount = Cls.prototype._applyCompositingMount;
if (typeof origMount === 'function') {
Cls.prototype._applyCompositingMount = function () {
activeInstances.add(this);
var res = origMount.apply(this, arguments);
if (!shouldEnable()) {
try {
this.setPaused(true);
this.clear();
} catch (err) {}
}
return res;
};
}
var origDestroy = Cls.prototype.destroy;
if (typeof origDestroy === 'function') {
Cls.prototype.destroy = function () {
activeInstances.delete(this);
return origDestroy.apply(this, arguments);
};
}
if (!shouldEnable()) {
pauseAll();
}
}
function ensureScriptLoadedAndPatched(callback) {
if (BAClickFXClass) {
if (callback) callback(BAClickFXClass);
return;
}
if (window.BAClickFX) {
patchBAClickFXClass(window.BAClickFX);
if (callback) callback(window.BAClickFX);
return;
}
if (isPatching) return;
isPatching = true;
try {
import(scriptUrl)
.then(function (module) {
isPatching = false;
var Cls = module.BAClickFX || module.default;
if (Cls) {
window.BAClickFX = Cls;
patchBAClickFXClass(Cls);
if (callback) callback(Cls);
}
})
.catch(function (error) {
isPatching = false;
console.error('[BAClickFX] 加载失败:', error);
});
} catch (error) {
isPatching = false;
console.error('[BAClickFX] 动态 import 不可用:', error);
}
}
function applyFx() {
var enabled = shouldEnable();
root.setAttribute('data-rmzas-fx', mode);
root.classList.toggle('rmzas-fx-disabled', !enabled);
if (selector) selector.value = mode;
if (!enabled) {
pauseAll();
return;
}
resumeAll();
// 如果还没有任何实例运行,则确保创建并挂载一个
if (activeInstances.size === 0) {
ensureScriptLoadedAndPatched(function (Cls) {
if (!shouldEnable() || activeInstances.size > 0) return;
try {
ensureTargetCanvas();
var instance = new Cls({
target: '#myCanvas',
trailAlways: true,
trailEnabled: true
});
activeInstances.add(instance);
} catch (error) {
console.error('[BAClickFX] 初始化失败:', error);
}
});
}
}
try {
mode = normalize(window.localStorage.getItem(storageKey));
} catch (error) {
// 存储不可用时当前页面内保留选择。
}
// 保证页面尽早有目标 Canvas
ensureTargetCanvas();
// 启动劫持逻辑
ensureScriptLoadedAndPatched();
// 尽早应用状态
applyFx();
// 暴露全局接口
window.__rmzasGetFxState = shouldEnable;
window.__rmzasRegisterBAClickFX = function (instance) {
activeInstances.add(instance);
if (!shouldEnable()) {
pauseAll();
}
};
window.addEventListener('storage', function (event) {
if (event.key === storageKey || event.key === null) {
mode = normalize(event.newValue);
applyFx();
}
});
function addFxControl() {
if (document.getElementById('rmzas-fx-select')) return;
var menu = document.querySelector('#p-personal ul, #p-personal .mw-portlet-body ul, #p-personal .body ul, #p-personal-inner ul');
var control = document.createElement(menu ? 'li' : 'div');
control.id = 'pt-rmzas-fx';
control.className = 'rmzas-fx-control';
if (!menu) control.classList.add('rmzas-fx-control-floating');
var label = document.createElement('label');
label.htmlFor = 'rmzas-fx-select';
label.textContent = '鼠标特效';
selector = document.createElement('select');
selector.id = 'rmzas-fx-select';
selector.title = '选择是否开启鼠标光标与点击特效(此浏览器会记住你的选择)';
[['on', '开启'], ['off', '关闭']].forEach(function (item) {
var option = document.createElement('option');
option.value = item[0];
option.textContent = item[1];
selector.appendChild(option);
});
selector.value = mode;
selector.addEventListener('change', function () {
mode = normalize(selector.value);
applyFx();
try {
window.localStorage.setItem(storageKey, mode);
} catch (error) {}
});
control.appendChild(label);
control.appendChild(selector);
var themeControl = document.getElementById('pt-rmzas-theme');
if (themeControl && themeControl.nextSibling) {
themeControl.parentNode.insertBefore(control, themeControl.nextSibling);
} else if (themeControl && themeControl.parentNode) {
themeControl.parentNode.appendChild(control);
} else if (menu) {
menu.appendChild(control);
} else {
document.body.appendChild(control);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addFxControl);
} else {
addFxControl();
}
})();
// 显示最近更新的页面列表(带时间)
setTimeout(function() {
//console.log('开始加载最近更新页面列表');
var sidebar = document.getElementById('mw-panel');
if (!sidebar) {
console.log('未找到侧边栏');
return;
}
var items = sidebar.getElementsByTagName('li');
var targetItem = null;
for (var i = 0; i < items.length; i++) {
if (items[i].textContent.indexOf('最近更新') !== -1) {
targetItem = items[i];
break;
}
}
if (!targetItem) {
console.log('未找到目标容器');
return;
}
targetItem.textContent = '';
targetItem.innerHTML = '最近更新:<br>加载中...';
// API请求添加timestamp参数,获取更新时间
var apiUrl = mw.util.wikiScript('api') + '?';
apiUrl += 'action=query';
apiUrl += '&list=recentchanges';
apiUrl += '&rcprop=title|timestamp'; // 同时获取标题和时间戳
apiUrl += '&rclimit=10';
apiUrl += '&rcdaylimit=30';
apiUrl += '&format=json';
fetch(apiUrl)
.then(function(response) {
if (!response.ok) throw new Error('API请求失败:' + response.status);
return response.json();
})
.then(function(data) {
targetItem.innerHTML = '最近更新:<br>';
if (!data.query || !data.query.recentchanges || data.query.recentchanges.length === 0) {
targetItem.innerHTML += '暂无更新';
return;
}
var seenTitles = new Set();
var uniqueUpdates = [];
for (var i = 0; i < data.query.recentchanges.length; i++) {
var page = data.query.recentchanges[i];
var title = page.title;
if (!seenTitles.has(title)) {
seenTitles.add(title);
uniqueUpdates.push(page); // 包含title和timestamp
if (uniqueUpdates.length >= 6) break;
}
}
var list = document.createElement('ul');
list.style.listStyle = 'none';
list.style.paddingLeft = '10px';
list.style.margin = '5px 0';
uniqueUpdates.forEach(function(page) {
// 处理时间戳:转换为本地时间(年-月-日 时:分)
var date = new Date(page.timestamp);
var timeStr = date.toLocaleString('zh-CN', {
month: '2-digit', // 月份(2位)
day: '2-digit', // 日期(2位)
hour: '2-digit', // 小时(2位)
minute: '2-digit', // 分钟(2位)
second: '2-digit', // 新增:显示秒(2位)
hour12: false // 强制24小时制(避免上午/下午)
}).replace(',', ''); // 移除可能的逗号分隔符
var listItem = document.createElement('li');
listItem.style.margin = '3px 0'; // 每条间距
var link = document.createElement('a');
link.href = mw.util.getUrl(page.title);
link.textContent = page.title;
// 时间显示(灰色小字)
var timeSpan = document.createElement('span');
timeSpan.textContent = ' (' + timeStr + ')';
timeSpan.className = 'rmzas-recent-time';
timeSpan.style.fontSize = '0.8em'; // 小字
timeSpan.style.marginLeft = '5px';
listItem.appendChild(link);
listItem.appendChild(timeSpan);
list.appendChild(listItem);
});
targetItem.appendChild(list);
})
.catch(function(error) {
console.error('加载失败:', error);
targetItem.innerHTML = '最近更新:<br>加载失败';
});
}, 1000);