MediaWiki:Common.js:修订间差异
SeniorDeveloper(留言 | 贡献) 优化自定义侧栏导航v2.1:健壮初始化、古风占位、错误降级 |
v3.1 修正 Minerva 抽屉挂载点 |
||
| (未显示3个用户的6个中间版本) | |||
| 第1行: | 第1行: | ||
/* 虾仁维基 - 自定义侧边栏导航 | /* 虾仁维基 - 自定义侧边栏导航 v3.0 */ | ||
/* | /* 优化:古风主题统一、健壮初始化、精致样式、移动端(Minerva)适配 */ | ||
/* 最后更新: 2026-07-30 */ | /* 最后更新: 2026-07-30 */ | ||
| 第16行: | 第16行: | ||
return; | return; | ||
} | } | ||
var skin = mw.config.get('skin'); | |||
var isMobile = (skin === 'minerva' || skin === 'minerva-vec'); | |||
mw.loader.using('mediawiki.api').then(function () { | mw.loader.using('mediawiki.api').then(function () { | ||
waitForPanel(function ($panel) { | if (isMobile) { | ||
waitForMobileDrawer(function ($host) { | |||
buildNav($host, true); | |||
}); | |||
} else { | |||
waitForPanel(function ($panel) { | |||
buildNav($panel, false); | |||
}); | |||
} | |||
}).catch(function (e) { | }).catch(function (e) { | ||
console.error('[虾仁导航] 依赖加载失败,降级构建', e); | console.error('[虾仁导航] 依赖加载失败,降级构建', e); | ||
waitForPanel(function ($panel) { | if (isMobile) { | ||
waitForMobileDrawer(function ($host) { | |||
buildNav($host, true); | |||
}); | |||
} else { | |||
waitForPanel(function ($panel) { | |||
buildNav($panel, false); | |||
}); | |||
} | |||
}); | }); | ||
} | } | ||
| 第41行: | 第55行: | ||
} | } | ||
function buildNav($panel) { | /* | ||
* Minerva 抽屉真实结构: | |||
* <nav class="navigation-drawer toggle-list"> | |||
* <input id="main-menu-input" class="toggle-list__checkbox"> <- checkbox hack 开关 | |||
* <label for="main-menu-input"> <- 汉堡按钮 | |||
* <div id="mw-mf-page-left" class="menu"> <- 抽屉内容, 由 #main-menu-input:checked ~ 控制显隐 | |||
* 必须挂到 #mw-mf-page-left 内部; 若挂到外层 <nav>, 内容会插在 checkbox 之前, | |||
* 脱离 checkbox hack 的兄弟选择器控制, 变成常驻页头, 破坏 header 布局。 | |||
*/ | |||
function waitForMobileDrawer(cb) { | |||
var tries = 0; | |||
function tryFind() { | |||
var $host = $('#mw-mf-page-left'); | |||
if (!$host.length) $host = $('.navigation-drawer .menu'); | |||
if (!$host.length) $host = $('.navigation-drawer #p-navigation').parent(); | |||
if ($host.length) return cb($host.first()); | |||
if (++tries > 60) return; | |||
setTimeout(tryFind, 100); | |||
} | |||
tryFind(); | |||
} | |||
function buildNav($panel, isMobile) { | |||
var html = ''; | var html = ''; | ||
| 第96行: | 第132行: | ||
html += '<div class="xn-single"><a href="' + url('共创入口') + '">共创入口</a></div>'; | html += '<div class="xn-single"><a href="' + url('共创入口') + '">共创入口</a></div>'; | ||
$panel.html(html); | if (isMobile) { | ||
var $box = $('<section class="xn-mobile"></section>'); | |||
$box.html(html); | |||
$panel.prepend($box); | |||
} else { | |||
$panel.html(html); | |||
} | |||
restoreCollapse(); | restoreCollapse(); | ||
bindEvents(); | bindEvents(); | ||
| 第151行: | 第193行: | ||
html += '<div class="xn-work">'; | html += '<div class="xn-work">'; | ||
html += '<div class="xn- | html += '<div class="xn-work-title xn-expandable" data-sid="' + workSid + '">' + work + '</div>'; | ||
html += '<div class="xn-body" id="xn-' + workSid + '">'; | html += '<div class="xn-body" id="xn-' + workSid + '">'; | ||
| 第229行: | 第271行: | ||
function highlightCurrent() { | function highlightCurrent() { | ||
var currentTitle = mw.config.get('wgPageName'); | var currentTitle = mw.config.get('wgPageName'); | ||
$(' | $('.xn-link a').each(function () { | ||
var href = $(this).attr('href'); | var href = $(this).attr('href'); | ||
if (!href) return; | if (!href) return; | ||
| 第308行: | 第350行: | ||
})(jQuery, mediaWiki); | })(jQuery, mediaWiki); | ||
/* ===== 虾仁AI 浮窗助手(2026-07-30 v2) ===== */ | |||
(function () { | |||
if (window.__xrAiInited) return; | |||
window.__xrAiInited = true; | |||
function xrAvatar(size) { | |||
return '<svg viewBox="0 0 48 48" width="' + size + '" height="' + size + '" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><ellipse cx="24" cy="14.5" rx="18" ry="5.5" fill="#caa53d"/><ellipse cx="24" cy="14.5" rx="18" ry="5.5" fill="none" stroke="#a8842a" stroke-width="0.8"/><path d="M13 14.5 Q24 2.5 35 14.5 Z" fill="#d9b84a"/><path d="M13 14.5 Q24 2.5 35 14.5 Z" fill="none" stroke="#a8842a" stroke-width="0.8"/><circle cx="12.5" cy="25" r="6.5" fill="#2c2c2c"/><circle cx="35.5" cy="25" r="6.5" fill="#2c2c2c"/><circle cx="24" cy="29" r="15" fill="#fdfdfd" stroke="#e9e2d2" stroke-width="0.8"/><ellipse cx="18" cy="28" rx="4.5" ry="5.6" fill="#2c2c2c"/><ellipse cx="30" cy="28" rx="4.5" ry="5.6" fill="#2c2c2c"/><circle cx="18" cy="28" r="1.9" fill="#fff"/><circle cx="30" cy="28" r="1.9" fill="#fff"/><ellipse cx="24" cy="33" rx="2.1" ry="1.5" fill="#2c2c2c"/><path d="M20.5 36 Q24 38.5 27.5 36" stroke="#2c2c2c" stroke-width="1.3" fill="none" stroke-linecap="round"/><circle cx="15" cy="33" r="2" fill="#c41e3a" opacity="0.18"/><circle cx="33" cy="33" r="2" fill="#c41e3a" opacity="0.18"/></svg>'; | |||
} | |||
function init() { | |||
var API = '/ai_proxy.php'; | |||
var TITLE = '虾仁AI'; | |||
var AUDIT_TEMPLATE = '作品名:\n1. 视觉识别(主角是否为草帽熊猫头或明显变体):\n2. 听觉识别(是否使用虾仁系列标志性AI配音):\n3. 主角身份(是否虾仁人格内核的投射,名字可不同):\n4. 完整性(有无完整结局):\n5. 深度(有无情感内核/名场面):\n6. 原创性(虾仁原生或深度改编):\n7. 自述与作品链接:\n'; | |||
var css = document.createElement('style'); | |||
css.textContent = [ | |||
'#xr-ai-fab{position:fixed;right:22px;bottom:22px;z-index:99999;width:56px;height:56px;border-radius:50%;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;background:linear-gradient(135deg,#c41e3a,#9b182d);color:#fff;font-size:26px;box-shadow:0 6px 20px rgba(155,24,45,.4);transition:transform .2s ease,box-shadow .2s ease;font-family:"Noto Serif SC","Songti SC",serif}', | |||
'#xr-ai-fab:hover{transform:scale(1.08);box-shadow:0 8px 26px rgba(155,24,45,.55)}', | |||
'#xr-ai-panel{position:fixed;right:22px;bottom:90px;z-index:99999;width:360px;max-width:calc(100vw - 32px);height:520px;max-height:calc(100vh - 120px);display:none;flex-direction:column;overflow:hidden;background:#fbf6ec;border:1px solid #e7d8bf;border-radius:14px;box-shadow:0 12px 40px rgba(80,40,30,.25);font-family:"Noto Serif SC","Source Han Serif SC","Songti SC","STSong","SimSun",serif;color:#3a2418}', | |||
'#xr-ai-panel.xr-show{display:flex}', | |||
'.xr-ai-head{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:linear-gradient(135deg,#c41e3a,#9b182d);color:#fff}', | |||
'.xr-ai-title{font-weight:700;font-size:16px;letter-spacing:.05em;display:flex;align-items:center;gap:6px}', | |||
'.xr-ai-close{background:none;border:none;color:#fff;font-size:20px;cursor:pointer;line-height:1}', | |||
'.xr-ai-tabs{display:flex;border-bottom:1px solid #ecdfc8;background:#f3e9d6}', | |||
'.xr-ai-tab{flex:1;padding:9px 0;text-align:center;cursor:pointer;font-size:13px;color:#7a5a3a;border:none;background:none;transition:color .2s}', | |||
'.xr-ai-tab.xr-active{color:#c41e3a;font-weight:700;box-shadow:inset 0 -2px 0 #c41e3a}', | |||
'.xr-ai-hint{text-align:center;font-size:11px;color:#a98c6b;padding:0 0 6px}', | |||
'.xr-ai-msgs{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px}', | |||
'.xr-ai-msg{max-width:84%;padding:9px 12px;border-radius:12px;line-height:1.6;font-size:14px;white-space:pre-wrap;word-break:break-word}', | |||
'.xr-ai-msg.xr-user{align-self:flex-end;background:#c41e3a;color:#fff;border-bottom-right-radius:3px}', | |||
'.xr-ai-msg.xr-ai{align-self:flex-start;background:#fff;color:#3a2418;border:1px solid #ecdfc8;border-left:3px solid #c41e3a;border-bottom-left-radius:3px}', | |||
'.xr-ai-inputbar{display:flex;gap:8px;padding:10px;border-top:1px solid #ecdfc8;background:#f6eddb}', | |||
'.xr-ai-input{flex:1;resize:none;border:1px solid #e0cda8;border-radius:9px;padding:8px 10px;font-family:inherit;font-size:14px;background:#fff;outline:none;max-height:80px}', | |||
'.xr-ai-input:focus{border-color:#c41e3a}', | |||
'.xr-ai-send{border:none;border-radius:9px;padding:0 16px;background:#9b182d;color:#fff;cursor:pointer;font-family:inherit;font-size:14px;transition:background .2s}', | |||
'.xr-ai-send:hover{background:#c41e3a}', | |||
'.xr-ai-send:disabled{opacity:.5;cursor:default}' | |||
].join('\n'); | |||
document.head.appendChild(css); | |||
var fab = document.createElement('button'); | |||
fab.id = 'xr-ai-fab'; fab.innerHTML = xrAvatar(38); fab.title = TITLE; fab.setAttribute('aria-label', TITLE); | |||
var panel = document.createElement('div'); | |||
panel.id = 'xr-ai-panel'; | |||
panel.innerHTML = | |||
'<div class="xr-ai-head"><span class="xr-ai-title">' + xrAvatar(22) + ' ' + TITLE + '</span>' + | |||
'<button class="xr-ai-close" title="收起">×</button></div>' + | |||
'<div class="xr-ai-tabs">' + | |||
'<button class="xr-ai-tab xr-active" data-mode="general">闲聊</button>' + | |||
'<button class="xr-ai-tab" data-mode="wiki">问虾仁宇宙</button>' + | |||
'<button class="xr-ai-tab" data-mode="audit">作品审核</button></div>' + | |||
'<div class="xr-ai-hint">内容由 AI 生成,仅供参考</div>' + | |||
'<div class="xr-ai-msgs"></div>' + | |||
'<div class="xr-ai-inputbar">' + | |||
'<textarea class="xr-ai-input" rows="1" placeholder="和虾仁AI说点什么…"></textarea>' + | |||
'<button class="xr-ai-send">发送</button></div>' + | |||
'<input type="text" name="company" tabindex="-1" autocomplete="off" ' + | |||
'style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0;" aria-hidden="true">'; | |||
document.body.appendChild(fab); | |||
document.body.appendChild(panel); | |||
var msgsEl = panel.querySelector('.xr-ai-msgs'); | |||
var inputEl = panel.querySelector('.xr-ai-input'); | |||
var sendEl = panel.querySelector('.xr-ai-send'); | |||
var closeEl = panel.querySelector('.xr-ai-close'); | |||
var tabs = panel.querySelectorAll('.xr-ai-tab'); | |||
var honey = panel.querySelector('input[name=company]'); | |||
var mode = 'general'; | |||
var history = []; | |||
var busy = false; | |||
fab.addEventListener('click', function () { panel.classList.toggle('xr-show'); }); | |||
closeEl.addEventListener('click', function () { panel.classList.remove('xr-show'); }); | |||
tabs.forEach(function (t) { | |||
t.addEventListener('click', function () { | |||
tabs.forEach(function (x) { x.classList.remove('xr-active'); }); | |||
t.classList.add('xr-active'); | |||
mode = t.getAttribute('data-mode'); | |||
if (mode === 'audit' && !inputEl.value.trim()) { inputEl.value = AUDIT_TEMPLATE; inputEl.focus(); } | |||
}); | |||
}); | |||
function scrollBottom() { msgsEl.scrollTop = msgsEl.scrollHeight; } | |||
function addBubble(cls, text) { | |||
var d = document.createElement('div'); | |||
d.className = 'xr-ai-msg ' + cls; | |||
d.textContent = text; | |||
msgsEl.appendChild(d); | |||
scrollBottom(); | |||
return d; | |||
} | |||
function callAI(msgs, m, onChunk) { | |||
return fetch(API, { | |||
method: 'POST', | |||
headers: { 'Content-Type': 'application/json' }, | |||
body: JSON.stringify({ mode: m, messages: msgs, company: honey.value }) | |||
}).then(function (res) { | |||
if (!res.ok) throw new Error('HTTP ' + res.status); | |||
var reader = res.body.getReader(); | |||
var decoder = new TextDecoder(); | |||
var buf = ''; | |||
function pump() { | |||
return reader.read().then(function (result) { | |||
if (result.done) return; | |||
buf += decoder.decode(result.value, { stream: true }); | |||
var idx; | |||
while ((idx = buf.indexOf('\n')) >= 0) { | |||
var line = buf.slice(0, idx).trim(); | |||
buf = buf.slice(idx + 1); | |||
if (!line) continue; | |||
try { | |||
var obj = JSON.parse(line); | |||
if (obj.content) onChunk(obj.content); | |||
if (obj.error) onChunk('(' + obj.error + ')'); | |||
} catch (e) {} | |||
} | |||
return pump(); | |||
}); | |||
} | |||
return pump(); | |||
}); | |||
} | |||
function send() { | |||
var text = inputEl.value.trim(); | |||
if (!text || busy) return; | |||
busy = true; sendEl.disabled = true; | |||
addBubble('xr-user', text); | |||
history.push({ role: 'user', content: text }); | |||
inputEl.value = ''; | |||
var aiBubble = addBubble('xr-ai', '…'); | |||
var acc = ''; | |||
callAI(history.slice(-12), mode, function (chunk) { | |||
acc += chunk; | |||
aiBubble.textContent = acc + '▌'; | |||
scrollBottom(); | |||
}).then(function () { | |||
aiBubble.textContent = acc || '(暂无回复)'; | |||
history.push({ role: 'assistant', content: acc }); | |||
busy = false; sendEl.disabled = false; scrollBottom(); | |||
}).catch(function (e) { | |||
aiBubble.textContent = '出错了:' + (e && e.message ? e.message : e); | |||
busy = false; sendEl.disabled = false; | |||
}); | |||
} | |||
sendEl.addEventListener('click', send); | |||
inputEl.addEventListener('keydown', function (e) { | |||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } | |||
}); | |||
addBubble('xr-ai', '你好,我是虾仁AI,可以陪你闲聊,也能帮你查「虾仁宇宙」的资料。选个模式开始吧~'); | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', init); | |||
} else { | |||
init(); | |||
} | |||
})(); | |||