MediaWiki:Common.js:修订间差异
MediaWiki界面页面
更多操作
无编辑摘要 |
无编辑摘要 |
||
| (未显示同一用户的2个中间版本) | |||
| 第37行: | 第37行: | ||
} | } | ||
if (tryRun()) return; | if (tryRun()) return; | ||
| 第54行: | 第53行: | ||
cleanup(timer, observer); | cleanup(timer, observer); | ||
}, timeoutMs); | }, timeoutMs); | ||
}; | |||
/** | |||
* 页面就绪后执行回调,替代重复的 readyState 检查。 | |||
* @param {Function} fn | |||
*/ | |||
window.Anachronism.ready = function (fn) { | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', fn); | |||
} else { | |||
fn(); | |||
} | |||
}; | |||
/** | |||
* 页面就绪后立即调用 bindFn,并持续监听 DOM 变化反复调用。 | |||
* bindFn 内部应自带"已绑定"标记,避免重复绑定。 | |||
* @param {Function} bindFn | |||
*/ | |||
window.Anachronism.bindOnReady = function (bindFn) { | |||
Anachronism.ready(function () { | |||
bindFn(); | |||
new MutationObserver(bindFn).observe(document.body, { | |||
childList: true, | |||
subtree: true | |||
}); | |||
}); | |||
}; | }; | ||
| 第114行: | 第140行: | ||
btn.innerHTML = | btn.innerHTML = | ||
'<span class="fx-swap__a rail-button__icon fx-text-glow">' + iconDefault + '</span>' + | |||
'<span class="fx-swap__b rail-button__icon fx-text-glow">' + iconActive + '</span>'; | |||
btn.addEventListener('click', function () { | btn.addEventListener('click', function () { | ||
| 第206行: | 第232行: | ||
============================================ */ | ============================================ */ | ||
Rail.register('navigation', { | Rail.register('navigation', { icon: '☰', label: '导航', template: 'Template:工具栏导航' }); | ||
Rail.register('claim', { icon: '▨', label: '公告', template: 'Template:工具栏公告' }); | |||
Rail.register('help', { icon: '?', label: '帮助', template: 'Template:工具栏帮助' }); | |||
Rail.register('settings', { icon: '⚙', label: '设置', template: 'Template:工具栏设置' }); | |||
}); | |||
Rail.register('claim', { | |||
}); | |||
Rail.register('help', { | |||
}); | |||
Rail.register('settings', { | |||
}); | |||
| 第235行: | 第242行: | ||
============================================ */ | ============================================ */ | ||
Anachronism.ready(function () { Rail.init(); }); | |||
| 第247行: | 第250行: | ||
(function () { | (function () { | ||
var HIDDEN_GROUPS = ['*', 'user', 'autoconfirmed', 'emailconfirmed']; | var HIDDEN_GROUPS = ['*', 'user', 'autoconfirmed', 'emailconfirmed']; | ||
var GROUP_NAME_FALLBACK = { | var GROUP_NAME_FALLBACK = { | ||
'sysop': '策展人', | 'sysop': '策展人', | ||
| 第297行: | 第298行: | ||
return HIDDEN_GROUPS.indexOf(g) === -1; | return HIDDEN_GROUPS.indexOf(g) === -1; | ||
}); | }); | ||
if (!visible.length) { | if (!visible.length) { | ||
if (groups.indexOf('user') !== -1) | if (groups.indexOf('user') !== -1) return buildGroupTag('user'); | ||
return '无'; | return '无'; | ||
} | } | ||
return visible.map(buildGroupTag).join(''); | return visible.map(buildGroupTag).join(''); | ||
} | } | ||
function isStaff(groups) { | function isStaff(groups) { | ||
return groups.indexOf('sysop') !== -1 || groups.indexOf('bureaucrat') !== -1; | return groups.indexOf('sysop') !== -1 || groups.indexOf('bureaucrat') !== -1; | ||
} | } | ||
function updateNotificationButton(count) { | function updateNotificationButton(count) { | ||
var btn = document.getElementById('rail-notification-btn'); | var btn = document.getElementById('rail-notification-btn'); | ||
| 第331行: | 第325行: | ||
return; | return; | ||
} | } | ||
new mw.Api().get({ | new mw.Api().get({ | ||
action: 'query', | action: 'query', | ||
| 第343行: | 第336行: | ||
} catch (e) {} | } catch (e) {} | ||
updateNotificationButton(count); | updateNotificationButton(count); | ||
}).fail(function () { | }).fail(function () {}); | ||
} | } | ||
| 第366行: | 第357行: | ||
var regTime = mw.config.get('wgUserRegistration'); | var regTime = mw.config.get('wgUserRegistration'); | ||
var actionButtons = [ | var actionButtons = [ | ||
' <a href="' + mw.util.getUrl('Special:Contributions/' + userName) + '" class="rail-action-button">贡献</a>', | ' <a href="' + mw.util.getUrl('Special:Contributions/' + userName) + '" class="rail-action-button">贡献</a>', | ||
| 第374行: | 第364行: | ||
' <a href="' + mw.util.getUrl('Special:ListUsers') + '" class="rail-action-button">全部用户</a>' | ' <a href="' + mw.util.getUrl('Special:ListUsers') + '" class="rail-action-button">全部用户</a>' | ||
]; | ]; | ||
if (isStaff(groups)) { | if (isStaff(groups)) { | ||
actionButtons.push( | actionButtons.push( | ||
| 第397行: | 第385行: | ||
].join(''); | ].join(''); | ||
fetchUnreadCount(); | fetchUnreadCount(); | ||
} | } | ||
| 第405行: | 第392行: | ||
if (!box || box.dataset.filled === '1') return; | if (!box || box.dataset.filled === '1') return; | ||
box.dataset.filled = '1'; | box.dataset.filled = '1'; | ||
if (mw.config.get('wgUserName')) { | if (mw.config.get('wgUserName')) { | ||
renderUser(box); | renderUser(box); | ||
| 第421行: | 第407行: | ||
} | } | ||
Anachronism.ready(observeRailPanel); | |||
})(); | })(); | ||
| 第445行: | 第427行: | ||
var details = document.getElementById('citizen-preferences-details'); | var details = document.getElementById('citizen-preferences-details'); | ||
if (details) | if (details) details.open = true; | ||
document.body.classList.add('citizen-preferences-shown'); | document.body.classList.add('citizen-preferences-shown'); | ||
var card = document.getElementById('citizen-preferences-dropdown__card'); | var card = document.getElementById('citizen-preferences-dropdown__card'); | ||
if (card && !card.querySelector('.citizen-preferences-close')) { | if (card && !card.querySelector('.citizen-preferences-close')) { | ||
| 第468行: | 第447行: | ||
} | } | ||
document.addEventListener('click', function (e) { | document.addEventListener('click', function (e) { | ||
if (!document.body.classList.contains('citizen-preferences-shown')) return; | if (!document.body.classList.contains('citizen-preferences-shown')) return; | ||
| 第481行: | 第459行: | ||
}); | }); | ||
document.addEventListener('keydown', function (e) { | document.addEventListener('keydown', function (e) { | ||
if (e.key !== 'Escape') return; | if (e.key !== 'Escape') return; | ||
| 第490行: | 第467行: | ||
}); | }); | ||
Anachronism.bindOnReady(bindRailPreferences); | |||
})(); | })(); | ||
| 第504行: | 第479行: | ||
var existing = document.querySelector('.comments-body'); | var existing = document.querySelector('.comments-body'); | ||
if (existing && existing.dataset.relocated === '1') { | if (existing && existing.dataset.relocated === '1') { | ||
return true; | return true; | ||
} | } | ||
| 第526行: | 第501行: | ||
/* ============================================ | /* ============================================ | ||
评论区:把回复嵌套到父评论内部 | 评论区:把回复嵌套到父评论内部 | ||
============================================ */ | ============================================ */ | ||
| 第567行: | 第535行: | ||
/* ============================================ | /* ============================================ | ||
隐藏 Special:ListGroupRights 里的"常客"行 | 隐藏 Special:ListGroupRights 里的"常客"行 | ||
============================================ */ | ============================================ */ | ||
| 第587行: | 第554行: | ||
} | } | ||
Anachronism.ready(hideRows); | |||
})(); | })(); | ||
| 第597行: | 第560行: | ||
/* ============================================ | /* ============================================ | ||
特殊页面"相关页面"菜单:按有无图标加类 | 特殊页面"相关页面"菜单:按有无图标加类 | ||
============================================ */ | ============================================ */ | ||
| 第626行: | 第581行: | ||
} | } | ||
Anachronism.ready(function () { | |||
Anachronism.waitFor(markAssociatedPageButtons, { timeoutMs: 5000 }); | Anachronism.waitFor(markAssociatedPageButtons, { timeoutMs: 5000 }); | ||
} | }); | ||
})(); | })(); | ||
| 第638行: | 第589行: | ||
/* ============================================ | /* ============================================ | ||
站点头部:Logo + 站点名 | 站点头部:Logo + 站点名 | ||
============================================ */ | ============================================ */ | ||
| 第658行: | 第600行: | ||
if (!container) return false; | if (!container) return false; | ||
var siteName = mw.config.get('wgSiteName') || 'Anachronism'; | var siteName = mw.config.get('wgSiteName') || 'Anachronism'; | ||
var logoUrl = ''; | var logoUrl = ''; | ||
var homeUrl = '/'; | var homeUrl = '/'; | ||
| 第678行: | 第618行: | ||
} | } | ||
if (!logoUrl) { | if (!logoUrl) { | ||
var logos = mw.config.get('wgLogos') || {}; | var logos = mw.config.get('wgLogos') || {}; | ||
| 第701行: | 第640行: | ||
} | } | ||
Anachronism.ready(function () { | |||
Anachronism.waitFor(buildHeader, { timeoutMs: 5000 }); | Anachronism.waitFor(buildHeader, { timeoutMs: 5000 }); | ||
} | }); | ||
})(); | })(); | ||
| 第720行: | 第655行: | ||
var MIN_DESKTOP = 1101; | var MIN_DESKTOP = 1101; | ||
var GAP = 24; | var GAP = 24; | ||
var COL_W = 320; | var COL_W = 320; | ||
var EDGE = 8; | var EDGE = 8; | ||
function ensureSideColumn(mwBody) { | function ensureSideColumn(mwBody) { | ||
| 第772行: | 第707行: | ||
var colLeft = Math.round(mwRect.right + GAP); | var colLeft = Math.round(mwRect.right + GAP); | ||
var maxLeft = window.innerWidth - COL_W - EDGE; | var maxLeft = window.innerWidth - COL_W - EDGE; | ||
if (colLeft > maxLeft) colLeft = maxLeft; | if (colLeft > maxLeft) colLeft = maxLeft; | ||
col.style.left = colLeft + 'px'; | col.style.left = colLeft + 'px'; | ||
} | } | ||
| 第786行: | 第719行: | ||
if (!ensureSideColumn(mwBody)) return false; | if (!ensureSideColumn(mwBody)) return false; | ||
if (!ensureToggle(mwBody)) return false; | if (!ensureToggle(mwBody)) return false; | ||
if (window.innerWidth >= MIN_DESKTOP) { | if (window.innerWidth >= MIN_DESKTOP) { | ||
document.body.classList.add('anachronism-aside-open'); | document.body.classList.add('anachronism-aside-open'); | ||
| 第806行: | 第738行: | ||
} | } | ||
Anachronism.ready(function () { | |||
Anachronism.waitFor(start, { timeoutMs: 5000 }); | Anachronism.waitFor(start, { timeoutMs: 5000 }); | ||
} | }); | ||
})(); | })(); | ||
/* ============================================ | /* ============================================ | ||
收缩大标题:点击底部栏展开面板 | 收缩大标题:点击底部栏展开面板 | ||
============================================ */ | ============================================ */ | ||
(function () { | |||
Anachronism.bindOnReady(function () { | |||
document.querySelectorAll('.fold').forEach(function (fold) { | |||
if (fold.dataset.foldBound === '1') return; | |||
fold.dataset.foldBound = '1'; | |||
var bar = fold.querySelector('.fold__bar'); | |||
if (!bar) return; | |||
bar.addEventListener('click', function () { | |||
fold.classList.toggle('is-open'); | |||
}); | }); | ||
} | }); | ||
}); | |||
/* ============================================ | /* ============================================ | ||
旋转变换:transition 驱动,累积角度 + 浮起放大 | 旋转变换:transition 驱动,累积角度 + 浮起放大 | ||
============================================ */ | ============================================ */ | ||
Anachronism.bindOnReady(function () { | |||
document.querySelectorAll('.fx-rotate').forEach(function (el) { | |||
if (el.dataset.rotateBound === '1') return; | |||
el.dataset.rotateBound = '1'; | |||
var style = getComputedStyle(el); | |||
var angle = parseFloat(style.getPropertyValue('--fx-rotate-angle')) || 90; | |||
var interval = (parseFloat(style.getPropertyValue('--fx-rotate-interval')) || 2) * 1000; | |||
var duration = (parseFloat(style.getPropertyValue('--fx-rotate-duration')) || 0.6) * 1000; | |||
var dir = parseFloat(style.getPropertyValue('--fx-rotate-direction')) || 1; | |||
var lift = parseFloat(style.getPropertyValue('--fx-rotate-lift')) || 2; | |||
var scale = parseFloat(style.getPropertyValue('--fx-rotate-scale')) || 1.15; | |||
var current = 0; | |||
var current = | setInterval(function () { | ||
var start = current; | |||
var end = current + angle * dir; | |||
var mid = (start + end) / 2; | |||
current = end; | |||
el.style.transform = | |||
'translateY(-' + lift + 'px) rotate(' + mid + 'deg) scale(' + scale + ')'; | |||
setTimeout(function () { | |||
el.style.transform = | el.style.transform = | ||
'translateY(0) rotate(' + end + 'deg) scale(1)'; | |||
}, duration / 2); | |||
}, interval); | |||
}); | |||
}); | |||
} | |||
2026年10月6日 (二) 17:38的最新版本
/* ============================================
通用工具
============================================ */
window.Anachronism = window.Anachronism || {};
/**
* 等待某个条件成立后执行任务。
* 三路触发:立即尝试、定时轮询、DOM 变化时重试。
* 任一成功或超时后自动清理。
*
* @param {Function} taskFn 返回 true 表示任务完成
* @param {Object} [options]
* @param {number} [options.maxAttempts=100] 轮询最大次数
* @param {number} [options.intervalMs=100] 轮询间隔
* @param {number} [options.timeoutMs=15000] 硬超时
*/
window.Anachronism.waitFor = function (taskFn, options) {
options = options || {};
var maxAttempts = options.maxAttempts || 100;
var intervalMs = options.intervalMs || 100;
var timeoutMs = options.timeoutMs || 15000;
var attempts = 0;
var done = false;
function cleanup(timer, observer) {
if (done) return;
done = true;
if (timer) clearInterval(timer);
if (observer) observer.disconnect();
}
function tryRun() {
if (done) return false;
if (taskFn()) return true;
return false;
}
if (tryRun()) return;
var timer = setInterval(function () {
if (tryRun() || ++attempts >= maxAttempts) {
cleanup(timer, observer);
}
}, intervalMs);
var observer = new MutationObserver(function () {
if (tryRun()) cleanup(timer, observer);
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(function () {
cleanup(timer, observer);
}, timeoutMs);
};
/**
* 页面就绪后执行回调,替代重复的 readyState 检查。
* @param {Function} fn
*/
window.Anachronism.ready = function (fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
fn();
}
};
/**
* 页面就绪后立即调用 bindFn,并持续监听 DOM 变化反复调用。
* bindFn 内部应自带"已绑定"标记,避免重复绑定。
* @param {Function} bindFn
*/
window.Anachronism.bindOnReady = function (bindFn) {
Anachronism.ready(function () {
bindFn();
new MutationObserver(bindFn).observe(document.body, {
childList: true,
subtree: true
});
});
};
/* ============================================
侧边工具栏 Rail 管理器
============================================ */
window.Rail = (function () {
var items = [];
var activeId = null;
var panelEl, overlayEl, buttonsEl, contentEl, titleEl;
function ensureDOM() {
if (document.getElementById('rail-container')) return;
buttonsEl = document.createElement('div');
buttonsEl.id = 'rail-container';
buttonsEl.className = 'rail-container';
overlayEl = document.createElement('div');
overlayEl.id = 'rail-overlay';
overlayEl.className = 'rail-overlay';
panelEl = document.createElement('aside');
panelEl.id = 'rail-panel';
panelEl.className = 'rail-panel';
panelEl.innerHTML = [
'<div class="rail-panel__header">',
' <span class="rail-panel__title"></span>',
' <button class="rail-panel__close" type="button" aria-label="关闭">×</button>',
'</div>',
'<div class="rail-panel__content" id="rail-panel-content"></div>'
].join('');
titleEl = panelEl.querySelector('.rail-panel__title');
contentEl = panelEl.querySelector('#rail-panel-content');
document.body.appendChild(overlayEl);
document.body.appendChild(panelEl);
document.body.appendChild(buttonsEl);
panelEl.querySelector('.rail-panel__close').addEventListener('click', close);
overlayEl.addEventListener('click', close);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') close();
});
}
function createButton(item) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'rail-button fx-swap fx-pulse';
btn.setAttribute('data-rail-id', item.id);
btn.setAttribute('aria-label', item.label);
btn.setAttribute('title', item.label);
var iconDefault = item.icon || '●';
var iconActive = item.iconActive || iconDefault;
btn.innerHTML =
'<span class="fx-swap__a rail-button__icon fx-text-glow">' + iconDefault + '</span>' +
'<span class="fx-swap__b rail-button__icon fx-text-glow">' + iconActive + '</span>';
btn.addEventListener('click', function () {
if (activeId === item.id) {
close();
} else {
open(item);
}
});
return btn;
}
function highlightButtons() {
var all = buttonsEl.querySelectorAll('.rail-button');
for (var i = 0; i < all.length; i++) {
var id = all[i].getAttribute('data-rail-id');
all[i].classList.toggle('is-active', id === activeId);
}
}
function open(item) {
activeId = item.id;
titleEl.textContent = item.label;
contentEl.innerHTML = '<p class="rail-panel__loading">加载中…</p>';
highlightButtons();
document.body.classList.add('rail-open');
if (item.html) {
contentEl.innerHTML = item.html;
return;
}
if (item.template) {
var api = new mw.Api();
api.get({
action: 'parse',
page: item.template,
prop: 'text',
formatversion: '2'
}).done(function (data) {
if (activeId !== item.id) return;
if (data && data.parse && data.parse.text) {
contentEl.innerHTML = data.parse.text;
} else {
contentEl.innerHTML = '<p>加载失败。</p>';
}
}).fail(function () {
if (activeId !== item.id) return;
contentEl.innerHTML = '<p>加载失败。</p>';
});
} else {
contentEl.innerHTML = '<p>未配置内容。</p>';
}
}
function close() {
activeId = null;
document.body.classList.remove('rail-open');
if (buttonsEl) highlightButtons();
}
function init() {
ensureDOM();
buttonsEl.innerHTML = '';
items.forEach(function (item) {
buttonsEl.appendChild(createButton(item));
});
}
return {
register: function (id, options) {
options = options || {};
items.push({
id: id,
icon: options.icon || '●',
label: options.label || id,
html: options.html || null,
template: options.template || null
});
},
init: init,
open: open,
close: close
};
})();
/* ============================================
注册工具栏按钮
============================================ */
Rail.register('navigation', { icon: '☰', label: '导航', template: 'Template:工具栏导航' });
Rail.register('claim', { icon: '▨', label: '公告', template: 'Template:工具栏公告' });
Rail.register('help', { icon: '?', label: '帮助', template: 'Template:工具栏帮助' });
Rail.register('settings', { icon: '⚙', label: '设置', template: 'Template:工具栏设置' });
/* ============================================
Rail 初始化
============================================ */
Anachronism.ready(function () { Rail.init(); });
/* ============================================
工具栏账户信息动态填充
============================================ */
(function () {
var HIDDEN_GROUPS = ['*', 'user', 'autoconfirmed', 'emailconfirmed'];
var GROUP_NAME_FALLBACK = {
'sysop': '策展人',
'bureaucrat': '行政员',
'interface-admin': '织界者',
'interface_admin': '织界者',
'allowguest': '观察者',
'user': '旅人'
};
function formatDate(ms) {
if (!ms) return '—';
var d = new Date(ms);
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
function isMissing(msg) {
if (!msg) return true;
return msg.indexOf('\u29FC') !== -1 || msg.indexOf('⧼') !== -1;
}
function getGroupName(g) {
var txt = mw.msg('group-' + g + '-member');
if (!isMissing(txt)) return txt;
return GROUP_NAME_FALLBACK[g] || g;
}
function getGroupUrl(g) {
var page = mw.msg('grouppage-' + g);
if (!isMissing(page)) {
return mw.util.getUrl(page);
}
return mw.util.getUrl('Anachronism:' + getGroupName(g));
}
function buildGroupTag(g) {
var name = getGroupName(g);
var url = getGroupUrl(g);
return '<a href="' + url + '" class="rail-group-tag">' + name + '</a>';
}
function getGroupHtml(groups) {
var visible = groups.filter(function (g) {
return HIDDEN_GROUPS.indexOf(g) === -1;
});
if (!visible.length) {
if (groups.indexOf('user') !== -1) return buildGroupTag('user');
return '无';
}
return visible.map(buildGroupTag).join('');
}
function isStaff(groups) {
return groups.indexOf('sysop') !== -1 || groups.indexOf('bureaucrat') !== -1;
}
function updateNotificationButton(count) {
var btn = document.getElementById('rail-notification-btn');
if (!btn) return;
if (typeof count === 'number' && count > 0) {
btn.textContent = '通知 (' + count + ')';
} else {
btn.textContent = '通知';
}
}
function fetchUnreadCount() {
var cached = mw.config.get('wgEchoNotifsUnreadCount');
if (typeof cached === 'number') {
updateNotificationButton(cached);
return;
}
new mw.Api().get({
action: 'query',
meta: 'notifications',
notprop: 'count',
notfilter: '!read'
}).done(function (data) {
var count = 0;
try {
count = data.query.notifications.count || 0;
} catch (e) {}
updateNotificationButton(count);
}).fail(function () {});
}
function renderGuest(box) {
box.innerHTML = [
'<div class="rail-account-title">账户</div>',
'<p class="rail-account-guest">您的身份是访客</p>',
'<div class="rail-account-actions">',
' <a href="' + mw.util.getUrl('Special:UserLogin') + '" class="rail-action-button">登录</a>',
' <a href="' + mw.util.getUrl('Special:RequestAccount') + '" class="rail-action-button">申请账户</a>',
' <a href="' + mw.util.getUrl('Special:ListUsers') + '" class="rail-action-button">全部用户</a>',
'</div>'
].join('');
}
function renderUser(box) {
var userName = mw.config.get('wgUserName');
var groups = mw.config.get('wgUserGroups') || [];
var editCount = mw.config.get('wgUserEditCount');
var regTime = mw.config.get('wgUserRegistration');
var actionButtons = [
' <a href="' + mw.util.getUrl('Special:Contributions/' + userName) + '" class="rail-action-button">贡献</a>',
' <a href="' + mw.util.getUrl('Special:Watchlist') + '" class="rail-action-button">监视列表</a>',
' <a href="' + mw.util.getUrl('User talk:' + userName) + '" class="rail-action-button">用户讨论</a>',
' <a href="' + mw.util.getUrl('Special:Notifications') + '" class="rail-action-button" id="rail-notification-btn">通知</a>',
' <a href="' + mw.util.getUrl('Special:ListUsers') + '" class="rail-action-button">全部用户</a>'
];
if (isStaff(groups)) {
actionButtons.push(
' <a href="' + mw.util.getUrl('Special:ConfirmAccounts') + '" class="rail-action-button">审批</a>'
);
}
box.innerHTML = [
'<div class="rail-account-title">账户:' + userName + '</div>',
'<div class="rail-account-groups"><span class="rail-account-groups__label">权限组:</span>' + getGroupHtml(groups) + '</div>',
'<div class="rail-account-actions">',
actionButtons.join('\n'),
'</div>',
'<div class="rail-account-stats">',
' <span>编辑数:' + (editCount != null ? editCount : '—') + '</span>',
' <span>加入时间:' + formatDate(regTime) + '</span>',
'</div>',
'<div class="rail-account-logout">',
' <a href="' + mw.util.getUrl('Special:UserLogout') + '" class="rail-action-button rail-action-button--danger">退出登录</a>',
'</div>'
].join('');
fetchUnreadCount();
}
function fillAccountInfo() {
var box = document.getElementById('rail-account-info');
if (!box || box.dataset.filled === '1') return;
box.dataset.filled = '1';
if (mw.config.get('wgUserName')) {
renderUser(box);
} else {
renderGuest(box);
}
}
function observeRailPanel() {
var target = document.getElementById('rail-panel-content');
if (!target) return;
var observer = new MutationObserver(fillAccountInfo);
observer.observe(target, { childList: true, subtree: true });
fillAccountInfo();
}
Anachronism.ready(observeRailPanel);
})();
/* ============================================
在 rail 面板中触发 Citizen 偏好设置面板
============================================ */
(function () {
function bindRailPreferences() {
var trigger = document.getElementById('rail-open-preferences');
if (!trigger || trigger.dataset.bound === '1') return;
trigger.dataset.bound = '1';
trigger.addEventListener('click', function () {
if (window.Rail && typeof Rail.close === 'function') {
Rail.close();
}
var details = document.getElementById('citizen-preferences-details');
if (details) details.open = true;
document.body.classList.add('citizen-preferences-shown');
var card = document.getElementById('citizen-preferences-dropdown__card');
if (card && !card.querySelector('.citizen-preferences-close')) {
var closeBtn = document.createElement('button');
closeBtn.className = 'citizen-preferences-close';
closeBtn.type = 'button';
closeBtn.textContent = '×';
closeBtn.setAttribute('aria-label', '关闭');
closeBtn.addEventListener('click', function () {
document.body.classList.remove('citizen-preferences-shown');
var d = document.getElementById('citizen-preferences-details');
if (d) d.open = false;
});
card.insertBefore(closeBtn, card.firstChild);
}
});
}
document.addEventListener('click', function (e) {
if (!document.body.classList.contains('citizen-preferences-shown')) return;
var card = document.getElementById('citizen-preferences-dropdown__card');
var trigger = document.getElementById('rail-open-preferences');
if (!card) return;
if (card.contains(e.target)) return;
if (trigger && trigger.contains(e.target)) return;
document.body.classList.remove('citizen-preferences-shown');
var details = document.getElementById('citizen-preferences-details');
if (details) details.open = false;
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
if (!document.body.classList.contains('citizen-preferences-shown')) return;
document.body.classList.remove('citizen-preferences-shown');
var details = document.getElementById('citizen-preferences-details');
if (details) details.open = false;
});
Anachronism.bindOnReady(bindRailPreferences);
})();
/* ============================================
评论区重定位:从主内容中移出,成为页面级独立块
============================================ */
(function () {
function tryRelocate() {
var existing = document.querySelector('.comments-body');
if (existing && existing.dataset.relocated === '1') {
return true;
}
var comments = document.querySelector('.mw-parser-output .comments-body');
if (!comments) return false;
var mwBody = document.querySelector('.mw-body');
if (!mwBody || !mwBody.parentNode) return false;
comments.dataset.relocated = '1';
comments.classList.add('comments-body--page-level');
mwBody.parentNode.insertBefore(comments, mwBody.nextSibling);
return true;
}
Anachronism.waitFor(tryRelocate);
})();
/* ============================================
评论区:把回复嵌套到父评论内部
============================================ */
(function () {
function nestReplies() {
var body = document.querySelector('.comments-body');
if (!body) return false;
if (body.dataset.repliesNested === '1') return true;
var items = Array.prototype.slice.call(body.querySelectorAll('.c-item'));
if (!items.length) return false;
var currentFull = null;
items.forEach(function (item) {
if (item.classList.contains('full')) {
currentFull = item;
} else if (item.classList.contains('reply') && currentFull) {
var targetContainer = currentFull.querySelector('.c-container');
if (targetContainer && item.parentElement !== targetContainer) {
targetContainer.appendChild(item);
item.classList.add('nested-in-parent');
}
}
});
body.dataset.repliesNested = '1';
return true;
}
Anachronism.waitFor(nestReplies);
})();
/* ============================================
隐藏 Special:ListGroupRights 里的"常客"行
============================================ */
(function () {
var HIDE_GROUP_NAMES = ['常客'];
function hideRows() {
var table = document.querySelector('.mw-listgrouprights-table');
if (!table) return;
table.querySelectorAll('tr').forEach(function (tr) {
var link = tr.querySelector('a');
if (!link) return;
var text = (link.textContent || '').trim();
if (HIDE_GROUP_NAMES.indexOf(text) !== -1) {
tr.style.display = 'none';
}
});
}
Anachronism.ready(hideRows);
})();
/* ============================================
特殊页面"相关页面"菜单:按有无图标加类
============================================ */
(function () {
function markAssociatedPageButtons() {
var buttons = document.querySelectorAll('#p-associated-pages .mw-list-item > a');
if (!buttons.length) return false;
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
if (btn.dataset.railMarked === '1') continue;
btn.dataset.railMarked = '1';
if (btn.querySelector('.citizen-ui-icon')) {
btn.classList.add('rail-has-icon');
} else {
btn.classList.add('rail-no-icon');
}
}
return true;
}
Anachronism.ready(function () {
Anachronism.waitFor(markAssociatedPageButtons, { timeoutMs: 5000 });
});
})();
/* ============================================
站点头部:Logo + 站点名
============================================ */
(function () {
var HEADER_ID = 'site-header';
function buildHeader() {
if (document.getElementById(HEADER_ID)) return true;
var container = document.querySelector('.citizen-page-container');
if (!container) return false;
var siteName = mw.config.get('wgSiteName') || 'Anachronism';
var logoUrl = '';
var homeUrl = '/';
var existingLogoImg = document.querySelector('.citizen-header__logo .mw-logo-icon')
|| document.querySelector('.mw-logo .mw-logo-icon')
|| document.querySelector('img.mw-logo-icon');
if (existingLogoImg) {
logoUrl = (existingLogoImg.getAttribute('src') || '').trim();
}
var existingLogoLink = document.querySelector('.citizen-header__logo a')
|| document.querySelector('a.mw-logo');
if (existingLogoLink) {
homeUrl = existingLogoLink.getAttribute('href') || homeUrl;
}
if (!logoUrl) {
var logos = mw.config.get('wgLogos') || {};
logoUrl = logos['1x'] || logos['svg'] || mw.config.get('wgLogo') || '';
}
var logoImg = logoUrl
? '<img class="site-header__logo" src="' + logoUrl + '" alt="">'
: '';
var header = document.createElement('header');
header.id = HEADER_ID;
header.className = 'site-header';
header.innerHTML =
'<a class="site-header__brand" href="' + homeUrl + '">' +
logoImg +
'<span class="site-header__name">' + siteName + '</span>' +
'</a>';
container.insertBefore(header, container.firstChild);
return true;
}
Anachronism.ready(function () {
Anachronism.waitFor(buildHeader, { timeoutMs: 5000 });
});
})();
/* ============================================
右侧独立侧栏 + 展开/收起按钮
============================================ */
(function () {
var COLUMN_ID = 'anachronism-side-column';
var TOGGLE_ID = 'anachronism-aside-toggle';
var MIN_DESKTOP = 1101;
var GAP = 24;
var COL_W = 320;
var EDGE = 8;
function ensureSideColumn(mwBody) {
if (document.getElementById(COLUMN_ID)) return true;
var aside = mwBody.querySelector('.citizen-page-aside');
var footer = mwBody.querySelector('.citizen-page-footer');
if (!aside && !footer) return false;
var col = document.createElement('div');
col.id = COLUMN_ID;
col.className = 'anachronism-side-column';
if (aside) col.appendChild(aside);
if (footer) col.appendChild(footer);
mwBody.appendChild(col);
return true;
}
function ensureToggle(mwBody) {
if (document.getElementById(TOGGLE_ID)) return true;
var btn = document.createElement('button');
btn.id = TOGGLE_ID;
btn.type = 'button';
btn.className = 'anachronism-aside-toggle';
btn.setAttribute('aria-label', '展开/收起页面信息');
btn.setAttribute('title', '展开/收起页面信息');
btn.setAttribute('aria-expanded', 'false');
btn.innerHTML = '<span class="anachronism-aside-toggle__icon">▸</span>';
btn.addEventListener('click', function () {
var isOpen = document.body.classList.toggle('anachronism-aside-open');
btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
mwBody.appendChild(btn);
return true;
}
function reposition() {
if (window.innerWidth < MIN_DESKTOP) return;
var col = document.getElementById(COLUMN_ID);
var mwBody = document.querySelector('.mw-body');
if (!col || !mwBody) return;
var mwRect = mwBody.getBoundingClientRect();
var colLeft = Math.round(mwRect.right + GAP);
var maxLeft = window.innerWidth - COL_W - EDGE;
if (colLeft > maxLeft) colLeft = maxLeft;
col.style.left = colLeft + 'px';
}
function start() {
var mwBody = document.querySelector('.mw-body');
if (!mwBody) return false;
if (!ensureSideColumn(mwBody)) return false;
if (!ensureToggle(mwBody)) return false;
if (window.innerWidth >= MIN_DESKTOP) {
document.body.classList.add('anachronism-aside-open');
var btn = document.getElementById(TOGGLE_ID);
if (btn) btn.setAttribute('aria-expanded', 'true');
}
reposition();
window.addEventListener('resize', reposition);
new MutationObserver(reposition).observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return true;
}
Anachronism.ready(function () {
Anachronism.waitFor(start, { timeoutMs: 5000 });
});
})();
/* ============================================
收缩大标题:点击底部栏展开面板
============================================ */
Anachronism.bindOnReady(function () {
document.querySelectorAll('.fold').forEach(function (fold) {
if (fold.dataset.foldBound === '1') return;
fold.dataset.foldBound = '1';
var bar = fold.querySelector('.fold__bar');
if (!bar) return;
bar.addEventListener('click', function () {
fold.classList.toggle('is-open');
});
});
});
/* ============================================
旋转变换:transition 驱动,累积角度 + 浮起放大
============================================ */
Anachronism.bindOnReady(function () {
document.querySelectorAll('.fx-rotate').forEach(function (el) {
if (el.dataset.rotateBound === '1') return;
el.dataset.rotateBound = '1';
var style = getComputedStyle(el);
var angle = parseFloat(style.getPropertyValue('--fx-rotate-angle')) || 90;
var interval = (parseFloat(style.getPropertyValue('--fx-rotate-interval')) || 2) * 1000;
var duration = (parseFloat(style.getPropertyValue('--fx-rotate-duration')) || 0.6) * 1000;
var dir = parseFloat(style.getPropertyValue('--fx-rotate-direction')) || 1;
var lift = parseFloat(style.getPropertyValue('--fx-rotate-lift')) || 2;
var scale = parseFloat(style.getPropertyValue('--fx-rotate-scale')) || 1.15;
var current = 0;
setInterval(function () {
var start = current;
var end = current + angle * dir;
var mid = (start + end) / 2;
current = end;
el.style.transform =
'translateY(-' + lift + 'px) rotate(' + mid + 'deg) scale(' + scale + ')';
setTimeout(function () {
el.style.transform =
'translateY(0) rotate(' + end + 'deg) scale(1)';
}, duration / 2);
}, interval);
});
});