✨🎊 Postal Wiki 三周年! 🎊✨
🎂 2026.09.11 — 邮路三载,百科同行
用户:Hehua/filepreview.js:修订间差异
来自Postal Wiki
< 用户:Hehua
创建页面,内容为“→* * FilePreview — 文件链接悬停预览脚本 * 鼠标悬停在 File: 蓝色链接上时,弹出浮窗显示: * - 缩略图预览 * - 文件信息(尺寸、大小、上传者) * - 分类 (categories) * - 版权/许可模板 (templates) * * 安装方式:将本脚本的内容复制到你的 * User:<你的名字>/common.js 或 MediaWiki:Common.js * 或通过 importScript 引入。: mw.loader.using(['mediawiki.api', 'mediawiki…” |
无编辑摘要 |
||
| 第7行: | 第7行: | ||
* - 版权/许可模板 (templates) | * - 版权/许可模板 (templates) | ||
* | * | ||
* | * 安装方式: | ||
* User:<你的名字>/common | * 将本脚本的内容复制到 User:<你的名字>/common.js | ||
* 或通过 importScript | * 或通过 importScript('User:你的名字/FilePreview.js'); 引入 | ||
*/ | */ | ||
mw.loader.using(['mediawiki.api', 'mediawiki.util'] | mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.user'], function () { | ||
'use strict'; | 'use strict'; | ||
console.log('[FilePreview] 脚本已加载'); | |||
var CONFIG = { | var CONFIG = { | ||
delay: 400, | delay: 400, | ||
thumbWidth: 220, | thumbWidth: 220, | ||
maxPopupWidth: 460, | maxPopupWidth: 460, | ||
maxPopupHeight: 520 | maxPopupHeight: 520 | ||
}; | }; | ||
var state = { | var state = { | ||
link: null, | link: null, | ||
| 第29行: | 第30行: | ||
}; | }; | ||
/* ---------- popup | /* ---------- utils ---------- */ | ||
function isFileLink(link) { | |||
if (!link || !link.href) return false; | |||
var title = link.getAttribute('title') || ''; | |||
if (/^(File|Image):/i.test(title)) return true; | |||
return /[?&]title=(File|Image):/i.test(link.href); | |||
} | |||
function getFileTitle(link) { | |||
var title = link.getAttribute('title') || ''; | |||
var m = title.match(/^(File|Image):(.+)$/i); | |||
if (m) return m[1] + ':' + m[2]; | |||
var href = link.getAttribute('href') || ''; | |||
m = href.match(/[?&]title=(File|Image):([^&]+)/); | |||
if (m) return m[1] + ':' + decodeURIComponent(m[2].replace(/\+/g, ' ')); | |||
return null; | |||
} | |||
function fmtBytes(b) { | |||
if (!b) return '?'; | |||
if (b < 1024) return b + ' B'; | |||
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'; | |||
return (b / 1048576).toFixed(1) + ' MB'; | |||
} | |||
function esc(s) { | |||
return mw.html.escape(String(s)); | |||
} | |||
/* ---------- popup ---------- */ | |||
function getPopup() { | function getPopup() { | ||
if (!state.popup) { | if (!state.popup) { | ||
| 第47行: | 第77行: | ||
'overflow-y:auto', | 'overflow-y:auto', | ||
'font:13px/1.5 sans-serif', | 'font:13px/1.5 sans-serif', | ||
'color:#222 | 'color:#222' | ||
].join(';'); | ].join(';'); | ||
document.body.appendChild(el); | document.body.appendChild(el); | ||
state.popup = el; | state.popup = el; | ||
el.addEventListener('mouseenter', function () { | el.addEventListener('mouseenter', function () { | ||
clearTimeout(state.hideTimer); | clearTimeout(state.hideTimer); | ||
| 第59行: | 第87行: | ||
}); | }); | ||
el.addEventListener('mouseleave', function (e) { | el.addEventListener('mouseleave', function (e) { | ||
if ( | if (state.link && e.relatedTarget && state.link.contains(e.relatedTarget)) return; | ||
hidePopup(); | hidePopup(); | ||
}); | }); | ||
| 第72行: | 第98行: | ||
popup.innerHTML = html; | popup.innerHTML = html; | ||
popup.style.display = 'block'; | popup.style.display = 'block'; | ||
var rect = popup.getBoundingClientRect(); | var rect = popup.getBoundingClientRect(); | ||
var left = Math.max(6, Math.min(x + 12, window.innerWidth - rect.width - 6)); | var left = Math.max(6, Math.min(x + 12, window.innerWidth - rect.width - 6)); | ||
| 第85行: | 第110行: | ||
if (state.popup) state.popup.style.display = 'none'; | if (state.popup) state.popup.style.display = 'none'; | ||
state.link = null; | state.link = null; | ||
} | } | ||
| 第123行: | 第118行: | ||
action: 'query', | action: 'query', | ||
titles: fileTitle, | titles: fileTitle, | ||
prop: 'categories|templates|imageinfo | prop: 'categories|templates|imageinfo', | ||
cllimit: 50, | cllimit: 50, | ||
tllimit: 50, | tllimit: 50, | ||
iiprop: 'url|size|user|comment|timestamp|extmetadata', | iiprop: 'url|size|user|comment|timestamp|extmetadata', | ||
iiurlwidth: CONFIG.thumbWidth, | iiurlwidth: CONFIG.thumbWidth, | ||
| 第148行: | 第142行: | ||
parts.push('<div style="font-weight:bold;font-size:14px;margin-bottom:6px;word-break:break-all;">' + esc(page.title) + '</div>'); | parts.push('<div style="font-weight:bold;font-size:14px;margin-bottom:6px;word-break:break-all;">' + esc(page.title) + '</div>'); | ||
var ii = page.imageinfo && page.imageinfo[0]; | var ii = page.imageinfo && page.imageinfo[0]; | ||
if (ii) { | if (ii) { | ||
| 第156行: | 第149行: | ||
} | } | ||
var meta = []; | var meta = []; | ||
if (ii.width && ii.height) meta.push(ii.width + ' | if (ii.width && ii.height) meta.push(ii.width + '\u00d7' + ii.height + ' (' + fmtBytes(ii.size) + ')'); | ||
if (ii.user) meta.push(' | if (ii.user) meta.push('\u4e0a\u4f20\u8005: ' + esc(ii.user)); | ||
if (meta.length) parts.push('<div style="color:#555;margin-bottom:4px;font-size:12px;">' + meta.join(' | ') + '</div>'); | if (meta.length) parts.push('<div style="color:#555;margin-bottom:4px;font-size:12px;">' + meta.join(' | ') + '</div>'); | ||
if (ii.extmetadata) { | if (ii.extmetadata) { | ||
var licField = ii.extmetadata.LicenseShortName || ii.extmetadata.License || ii.extmetadata.Copyright; | var licField = ii.extmetadata.LicenseShortName || ii.extmetadata.License || ii.extmetadata.Copyright; | ||
if (licField) { | if (licField) { | ||
var licVal = licField.value || licField; | var licVal = licField.value || licField; | ||
parts.push('<div style="color:#555;margin-bottom:4px;font-size:12px;"> | parts.push('<div style="color:#555;margin-bottom:4px;font-size:12px;">\u8bb8\u53ef: ' + esc(String(licVal)) + '</div>'); | ||
} | } | ||
} | } | ||
} | } | ||
// | // categories | ||
if (page.categories && page.categories.length) { | if (page.categories && page.categories.length) { | ||
parts.push('<div style="margin-top:8px;font-weight:bold;font-size:13px;"> | parts.push('<div style="margin-top:8px;font-weight:bold;font-size:13px;">\u5206\u7c7b</div>'); | ||
parts.push('<ul style="margin:2px 0 0 14px;padding:0;list-style:none;">'); | parts.push('<ul style="margin:2px 0 0 14px;padding:0;list-style:none;">'); | ||
page.categories.slice(0, 25).forEach(function (c) { | |||
parts.push('<li style="padding:1px 0;font-size:12px;">\u2022 ' + esc(c.title.replace(/^Category:/, '')) + '</li>'); | |||
parts.push('<li style="padding:1px 0;font-size:12px;"> | |||
}); | }); | ||
if (page.categories.length > 25) { | if (page.categories.length > 25) { | ||
parts.push('<li style="color:#888;font-size:12px;"> | parts.push('<li style="color:#888;font-size:12px;">\u2026 \u8fd8\u6709 ' + (page.categories.length - 25) + ' \u4e2a</li>'); | ||
} | } | ||
parts.push('</ul>'); | parts.push('</ul>'); | ||
} else { | } else { | ||
parts.push('<div style="margin-top:6px;color:#888;font-size:12px;"> | parts.push('<div style="margin-top:6px;color:#888;font-size:12px;">\u65e0\u5206\u7c7b</div>'); | ||
} | } | ||
// | // templates | ||
if (page.templates && page.templates.length) { | if (page.templates && page.templates.length) { | ||
parts.push('<div style="margin-top:8px;font-weight:bold;font-size:13px;"> | parts.push('<div style="margin-top:8px;font-weight:bold;font-size:13px;">\u6a21\u677f (\u542b\u7248\u6743/\u8bb8\u53ef)</div>'); | ||
parts.push('<div style="margin:2px 0 0;max-height:130px;overflow-y:auto;">'); | parts.push('<div style="margin:2px 0 0;max-height:130px;overflow-y:auto;">'); | ||
page.templates.slice(0, 40).forEach(function (t) { | |||
var name = t.title.replace(/^Template:/, ''); | var name = t.title.replace(/^Template:/, ''); | ||
parts.push('<span style="display:inline-block;background:#f0f2f5;border:1px solid #d4d8dd;border-radius:3px;padding:1px 5px;margin:1px 2px;font:12px monospace;color:#333;">{{' + esc(name) + '}}</span>'); | parts.push('<span style="display:inline-block;background:#f0f2f5;border:1px solid #d4d8dd;border-radius:3px;padding:1px 5px;margin:1px 2px;font:12px monospace;color:#333;">{{' + esc(name) + '}}</span>'); | ||
}); | }); | ||
if (page.templates.length > 40) { | if (page.templates.length > 40) { | ||
parts.push('<span style="color:#888;font-size:11px;"> | parts.push('<span style="color:#888;font-size:11px;"> \u2026+' + (page.templates.length - 40) + '</span>'); | ||
} | } | ||
parts.push('</div>'); | parts.push('</div>'); | ||
} | } | ||
// | // footer link | ||
parts.push('<div style="margin-top:10px;border-top:1px solid #eaecf0;padding-top:6px;text-align:right;font-size:12px;">' + | parts.push('<div style="margin-top:10px;border-top:1px solid #eaecf0;padding-top:6px;text-align:right;font-size:12px;">' + | ||
'<a href="' + | '<a href="' + mw.util.getUrl(page.title) + '" target="_blank">\u6253\u5f00\u6587\u4ef6\u9875 \u2192</a></div>'); | ||
showPopup(x, y, parts.join('')); | showPopup(x, y, parts.join('')); | ||
} | } | ||
/* ---------- | /* ---------- event handlers ---------- */ | ||
var currentMouseX = 0, currentMouseY = 0; | |||
document.addEventListener('mousemove', function (e) { | |||
currentMouseX = e.clientX; | |||
currentMouseY = e.clientY; | |||
}, true); | |||
function onLinkOver(e) { | function onLinkOver(e) { | ||
var link = e.target.closest ? e.target.closest('a | var link = e.target.closest ? e.target.closest('a') : null; | ||
if (!link || !isFileLink(link)) return; | if (!link || !isFileLink(link)) return; | ||
if (link === state.link) return; | if (link === state.link) return; | ||
| 第221行: | 第216行: | ||
var title = getFileTitle(link); | var title = getFileTitle(link); | ||
if (!title) return; | if (!title) { | ||
console.log('[FilePreview] 无法解析文件标题:', link.href); | |||
return; | |||
} | |||
console.log('[FilePreview] 悬停:', title); | |||
var mx = currentMouseX, my = currentMouseY; | |||
state.fetchTimer = setTimeout(function () { | state.fetchTimer = setTimeout(function () { | ||
showPopup( | showPopup(mx, my, '<div style="color:#888;font-size:13px;">\u52a0\u8f7d\u4e2d\u2026 ' + esc(title) + '</div>'); | ||
fetchFileData(title).then(function (data) { | fetchFileData(title).then(function (data) { | ||
if (state.link === link) renderPopup(data, | if (state.link === link) renderPopup(data, mx, my); | ||
}).catch(function (err) { | }).catch(function (err) { | ||
if (state.link === link) showPopup( | console.log('[FilePreview] API 错误:', err); | ||
if (state.link === link) showPopup(mx, my, '<div style="color:#c00;">\u8bf7\u6c42\u5931\u8d25: ' + esc(String(err)) + '</div>'); | |||
}); | }); | ||
}, CONFIG.delay); | }, CONFIG.delay); | ||
| 第234行: | 第236行: | ||
function onLinkOut(e) { | function onLinkOut(e) { | ||
var link = e.target.closest ? e.target.closest('a | var link = e.target.closest ? e.target.closest('a') : null; | ||
if (!link || !isFileLink(link)) return; | if (!link || !isFileLink(link) || link !== state.link) return; | ||
if (link | |||
// 如果鼠标移到了 <a> 内部的子元素(如 <img>),不隐藏 | |||
if (e.relatedTarget && link.contains(e.relatedTarget)) return; | |||
clearTimeout(state.fetchTimer); | clearTimeout(state.fetchTimer); | ||
state.hideTimer = setTimeout(function () { | state.hideTimer = setTimeout(function () { | ||
| 第248行: | 第251行: | ||
document.addEventListener('mouseover', onLinkOver, true); | document.addEventListener('mouseover', onLinkOver, true); | ||
document.addEventListener('mouseout', onLinkOut, true); | document.addEventListener('mouseout', onLinkOut, true); | ||
window.addEventListener('scroll', function () { hidePopup(); }, true); | |||
console.log('[FilePreview] 事件监听已安装'); | |||
}); | }); | ||
2026年7月23日 (四) 06:37的版本
/**
* FilePreview — 文件链接悬停预览脚本
* 鼠标悬停在 File: 蓝色链接上时,弹出浮窗显示:
* - 缩略图预览
* - 文件信息(尺寸、大小、上传者)
* - 分类 (categories)
* - 版权/许可模板 (templates)
*
* 安装方式:
* 将本脚本的内容复制到 User:<你的名字>/common.js
* 或通过 importScript('User:你的名字/FilePreview.js'); 引入
*/
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.user'], function () {
'use strict';
console.log('[FilePreview] 脚本已加载');
var CONFIG = {
delay: 400,
thumbWidth: 220,
maxPopupWidth: 460,
maxPopupHeight: 520
};
var state = {
link: null,
fetchTimer: null,
hideTimer: null,
popup: null
};
/* ---------- utils ---------- */
function isFileLink(link) {
if (!link || !link.href) return false;
var title = link.getAttribute('title') || '';
if (/^(File|Image):/i.test(title)) return true;
return /[?&]title=(File|Image):/i.test(link.href);
}
function getFileTitle(link) {
var title = link.getAttribute('title') || '';
var m = title.match(/^(File|Image):(.+)$/i);
if (m) return m[1] + ':' + m[2];
var href = link.getAttribute('href') || '';
m = href.match(/[?&]title=(File|Image):([^&]+)/);
if (m) return m[1] + ':' + decodeURIComponent(m[2].replace(/\+/g, ' '));
return null;
}
function fmtBytes(b) {
if (!b) return '?';
if (b < 1024) return b + ' B';
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
return (b / 1048576).toFixed(1) + ' MB';
}
function esc(s) {
return mw.html.escape(String(s));
}
/* ---------- popup ---------- */
function getPopup() {
if (!state.popup) {
var el = document.createElement('div');
el.id = 'filepreview-popup';
el.style.cssText = [
'display:none',
'position:fixed',
'z-index:9999',
'background:#fff',
'border:1px solid #c8ccd1',
'border-radius:6px',
'box-shadow:0 4px 16px rgba(0,0,0,0.2)',
'padding:14px',
'max-width:' + CONFIG.maxPopupWidth + 'px',
'max-height:' + CONFIG.maxPopupHeight + 'px',
'overflow-y:auto',
'font:13px/1.5 sans-serif',
'color:#222'
].join(';');
document.body.appendChild(el);
state.popup = el;
el.addEventListener('mouseenter', function () {
clearTimeout(state.hideTimer);
clearTimeout(state.fetchTimer);
});
el.addEventListener('mouseleave', function (e) {
if (state.link && e.relatedTarget && state.link.contains(e.relatedTarget)) return;
hidePopup();
});
}
return state.popup;
}
function showPopup(x, y, html) {
var popup = getPopup();
popup.innerHTML = html;
popup.style.display = 'block';
var rect = popup.getBoundingClientRect();
var left = Math.max(6, Math.min(x + 12, window.innerWidth - rect.width - 6));
var top = Math.max(6, Math.min(y + 12, window.innerHeight - rect.height - 6));
popup.style.left = left + 'px';
popup.style.top = top + 'px';
}
function hidePopup() {
clearTimeout(state.hideTimer);
clearTimeout(state.fetchTimer);
if (state.popup) state.popup.style.display = 'none';
state.link = null;
}
/* ---------- API ---------- */
function fetchFileData(fileTitle) {
var api = new mw.Api();
return api.get({
action: 'query',
titles: fileTitle,
prop: 'categories|templates|imageinfo',
cllimit: 50,
tllimit: 50,
iiprop: 'url|size|user|comment|timestamp|extmetadata',
iiurlwidth: CONFIG.thumbWidth,
format: 'json'
});
}
/* ---------- render ---------- */
function renderPopup(data, x, y) {
var pages = data.query && data.query.pages;
if (!pages) { showPopup(x, y, '<div style="color:#c00;">无法获取数据</div>'); return; }
var id = Object.keys(pages)[0];
var page = pages[id];
if (id === '-1' || !page) {
showPopup(x, y, '<div style="color:#888;">文件页面不存在</div>');
return;
}
var parts = [];
parts.push('<div style="font-weight:bold;font-size:14px;margin-bottom:6px;word-break:break-all;">' + esc(page.title) + '</div>');
var ii = page.imageinfo && page.imageinfo[0];
if (ii) {
if (ii.thumburl) {
parts.push('<div style="text-align:center;margin-bottom:8px;"><a href="' + esc(ii.url) + '" target="_blank">' +
'<img src="' + esc(ii.thumburl) + '" alt="" style="max-width:100%;border:1px solid #eaecf0;border-radius:3px;"></a></div>');
}
var meta = [];
if (ii.width && ii.height) meta.push(ii.width + '\u00d7' + ii.height + ' (' + fmtBytes(ii.size) + ')');
if (ii.user) meta.push('\u4e0a\u4f20\u8005: ' + esc(ii.user));
if (meta.length) parts.push('<div style="color:#555;margin-bottom:4px;font-size:12px;">' + meta.join(' | ') + '</div>');
if (ii.extmetadata) {
var licField = ii.extmetadata.LicenseShortName || ii.extmetadata.License || ii.extmetadata.Copyright;
if (licField) {
var licVal = licField.value || licField;
parts.push('<div style="color:#555;margin-bottom:4px;font-size:12px;">\u8bb8\u53ef: ' + esc(String(licVal)) + '</div>');
}
}
}
// categories
if (page.categories && page.categories.length) {
parts.push('<div style="margin-top:8px;font-weight:bold;font-size:13px;">\u5206\u7c7b</div>');
parts.push('<ul style="margin:2px 0 0 14px;padding:0;list-style:none;">');
page.categories.slice(0, 25).forEach(function (c) {
parts.push('<li style="padding:1px 0;font-size:12px;">\u2022 ' + esc(c.title.replace(/^Category:/, '')) + '</li>');
});
if (page.categories.length > 25) {
parts.push('<li style="color:#888;font-size:12px;">\u2026 \u8fd8\u6709 ' + (page.categories.length - 25) + ' \u4e2a</li>');
}
parts.push('</ul>');
} else {
parts.push('<div style="margin-top:6px;color:#888;font-size:12px;">\u65e0\u5206\u7c7b</div>');
}
// templates
if (page.templates && page.templates.length) {
parts.push('<div style="margin-top:8px;font-weight:bold;font-size:13px;">\u6a21\u677f (\u542b\u7248\u6743/\u8bb8\u53ef)</div>');
parts.push('<div style="margin:2px 0 0;max-height:130px;overflow-y:auto;">');
page.templates.slice(0, 40).forEach(function (t) {
var name = t.title.replace(/^Template:/, '');
parts.push('<span style="display:inline-block;background:#f0f2f5;border:1px solid #d4d8dd;border-radius:3px;padding:1px 5px;margin:1px 2px;font:12px monospace;color:#333;">{{' + esc(name) + '}}</span>');
});
if (page.templates.length > 40) {
parts.push('<span style="color:#888;font-size:11px;"> \u2026+' + (page.templates.length - 40) + '</span>');
}
parts.push('</div>');
}
// footer link
parts.push('<div style="margin-top:10px;border-top:1px solid #eaecf0;padding-top:6px;text-align:right;font-size:12px;">' +
'<a href="' + mw.util.getUrl(page.title) + '" target="_blank">\u6253\u5f00\u6587\u4ef6\u9875 \u2192</a></div>');
showPopup(x, y, parts.join(''));
}
/* ---------- event handlers ---------- */
var currentMouseX = 0, currentMouseY = 0;
document.addEventListener('mousemove', function (e) {
currentMouseX = e.clientX;
currentMouseY = e.clientY;
}, true);
function onLinkOver(e) {
var link = e.target.closest ? e.target.closest('a') : null;
if (!link || !isFileLink(link)) return;
if (link === state.link) return;
clearTimeout(state.fetchTimer);
clearTimeout(state.hideTimer);
state.link = link;
var title = getFileTitle(link);
if (!title) {
console.log('[FilePreview] 无法解析文件标题:', link.href);
return;
}
console.log('[FilePreview] 悬停:', title);
var mx = currentMouseX, my = currentMouseY;
state.fetchTimer = setTimeout(function () {
showPopup(mx, my, '<div style="color:#888;font-size:13px;">\u52a0\u8f7d\u4e2d\u2026 ' + esc(title) + '</div>');
fetchFileData(title).then(function (data) {
if (state.link === link) renderPopup(data, mx, my);
}).catch(function (err) {
console.log('[FilePreview] API 错误:', err);
if (state.link === link) showPopup(mx, my, '<div style="color:#c00;">\u8bf7\u6c42\u5931\u8d25: ' + esc(String(err)) + '</div>');
});
}, CONFIG.delay);
}
function onLinkOut(e) {
var link = e.target.closest ? e.target.closest('a') : null;
if (!link || !isFileLink(link) || link !== state.link) return;
// 如果鼠标移到了 <a> 内部的子元素(如 <img>),不隐藏
if (e.relatedTarget && link.contains(e.relatedTarget)) return;
clearTimeout(state.fetchTimer);
state.hideTimer = setTimeout(function () {
hidePopup();
}, 200);
}
/* ---------- install ---------- */
document.addEventListener('mouseover', onLinkOver, true);
document.addEventListener('mouseout', onLinkOut, true);
window.addEventListener('scroll', function () { hidePopup(); }, true);
console.log('[FilePreview] 事件监听已安装');
});