MediaWiki:Common.js:修订间差异

SeniorDeveloper​(留言 | 贡献)
优化自定义侧栏导航v2.1:健壮初始化、古风占位、错误降级
虾仁​(留言 | 贡献)
v3.1 修正 Minerva 抽屉挂载点
 
(未显示3个用户的6个中间版本)
第1行: 第1行:
/* 虾仁维基 - 自定义侧边栏导航 v2.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) {
                buildNav($panel);
                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) {
                buildNav($panel);
                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-header xn-expandable" data-sid="' + workSid + '">' + work + '</div>';
                 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');
         $('#mw-panel .xn-link a').each(function () {
         $('.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();
  }
})();