MediaWiki:Common.js:修订间差异

来自帝鹰维基
跳转到导航 跳转到搜索
(v17)
(v27-topnav)
 
(未显示同一用户的3个中间版本)
第7行: 第7行:
   /* ---------- ① 顶部主导航:改入口就在 links 列表里,一行一个 ---------- */
   /* ---------- ① 顶部主导航:改入口就在 links 列表里,一行一个 ---------- */
   var links = [
   var links = [
     ['主站', 'Main_Page'],
     ['主站', '首页'],
     ['成员专区', '帝鹰核心成员'],
     ['成员专区', '帝鹰核心成员'],
     ['组织章程', '组织章程'],
     ['组织章程', '组织章程'],
第111行: 第111行:
         return;
         return;
       }
       }
       // 可视化编辑器/普通页面:生成模板代码 + 使用说明
       // ★ 可视化编辑器(VE):直接往编辑区插入带样式的文字(所见即所得),
      //  不再让用户去插模板——这是之前「字色切换没成功显示」的根因。
      //  两步法(实测最稳):① insertContent 插纯文本(选区自动覆盖新文字)
      //  ② annotateContent('set', 注解) 给选区套样式。
      //  注解必须由 ve.dm.annotationFactory.create 生成,不能用裸对象。
      var veSurface = window.ve && ve.init && ve.init.target && ve.init.target.surface;
      if (veSurface) {
        try {
          veSurface.getView().focus();                      // 聚焦编辑区,插入到当前光标
          var veModel = veSurface.getModel();
          var veFrag = veModel.getFragment();
          veFrag.insertContent(text);                        // ① 插入纯文本,选区自动落在文字上
          var veAnn = ve.dm.annotationFactory.create('textStyle/span',
            { attributes: { style: style } });
          veFrag.annotateContent('set', veAnn);              // ② 给选区加样式注解
          ov.remove();
          return;
        } catch (err) { /* VE 插入失败则走下面的模板代码兜底 */ }
      }
      // 普通页面(非编辑状态):生成模板代码 + 使用说明
       var wikitext = font
       var wikitext = font
         ? '{{字体样式|字体=' + font + '|字号=' + (size || '') + '|文字=' + text + '}}'
         ? '{{字体样式|字体=' + font + '|字号=' + (size || '') + '|文字=' + text + '}}'
         : '{{彩色文字|颜色=' + color + '|字号=' + (size || '') + '|文字=' + text + '}}';
         : '{{彩色文字|颜色=' + color + '|字号=' + (size || '') + '|文字=' + text + '}}';
       var tip = ov.querySelector('#dy-f-tip');
       var tip = ov.querySelector('#dy-f-tip');
       tip.innerHTML = '生成代码(已复制):<br><code>' + wikitext.replace(/</g, '&lt;') + '</code><br>' +
       tip.innerHTML = '请先进入编辑状态再点「🎨 文字样式」即可直接插入。<br>' +
        '可视化编辑器用法:点「插入 → 模板」→ 搜「彩色文字」或「字体样式」→ 按上面参数填写即可。';
        '生成代码(已复制):<code>' + wikitext.replace(/</g, '&lt;') + '</code>';
       try { navigator.clipboard.writeText(wikitext); } catch (e) {}
       try { navigator.clipboard.writeText(wikitext); } catch (e) {}
     });
     });
第277行: 第296行:
     else panel.appendChild(box);
     else panel.appendChild(box);


     /* 联系窗口:联系信息 + 快捷去聊天室 */
     /* 联系窗口:管理事务通过管理员账号登录后台处理;即时交流走聊天室 */
     box.querySelector('#dy-contact-open').addEventListener('click', function () {
     box.querySelector('#dy-contact-open').addEventListener('click', function () {
       var ov = dyDialog('📞 联系窗口',
       var ov = dyDialog('📞 联系窗口',
         '<div class="dy-ct-item"><b>管理员邮箱:</b>2361226568@qq.com</div>' +
         '<div class="dy-ct-item"><b>站务/词条管理:</b>管理员账号密码登录本站即可直接处理</div>' +
         '<div class="dy-ct-item"><b>即时交流:</b>帝鹰聊天室(侧边栏💬进入)</div>' +
         '<div class="dy-ct-item"><b>即时交流:</b>帝鹰聊天室(全员实时在线)</div>' +
         '<div class="dy-ct-item"><b>商务/入会:</b>请邮件联系并注明来意</div>' +
         '<div class="dy-ct-item"><b>咨询求助:</b>侧边栏「🤖 咨询窗口」AI 助手随时解答</div>' +
         '<button id="dy-ct-chat" class="dy-fbtn">💬 前往帝鹰聊天室</button>');
         '<button id="dy-ct-chat" class="dy-fbtn">💬 前往帝鹰聊天室</button>');
       ov.querySelector('#dy-ct-chat').addEventListener('click', function () {
       ov.querySelector('#dy-ct-chat').addEventListener('click', function () {

2026年10月22日 (四) 23:27的最新版本

/* =====================================================================
   帝鹰维基 · 全站脚本 v13
   模块:① 顶部主导航  ② 管理员可视化面板(直传图片+文字样式)
         ③ ♪ 帝鹰电台  ④ 💬 聊天室  ⑤ 📞 联系入口(联系窗口 + 🤖 AI 咨询窗口)
   ===================================================================== */
(function () {
  /* ---------- ① 顶部主导航:改入口就在 links 列表里,一行一个 ---------- */
  var links = [
    ['主站', '首页'],
    ['成员专区', '帝鹰核心成员'],
    ['组织章程', '组织章程'],
    ['史料归档', '聊天室记录归档'],
    ['分类', 'Special:Categories']
  ];
  function buildNav() {
    if (document.getElementById('dy-topnav')) return;
    var nav = document.createElement('nav');
    nav.id = 'dy-topnav';
    links.forEach(function (it) {
      var a = document.createElement('a');
      a.href = mw.util.getUrl(it[1]);
      a.textContent = it[0];
      nav.appendChild(a);
    });
    var pageBase = document.getElementById('mw-page-base');
    if (pageBase && pageBase.parentNode) {
      pageBase.parentNode.insertBefore(nav, pageBase);
    } else {
      document.body.insertBefore(nav, document.body.firstChild);
    }
  }

  /* ---------- ② 管理员专属:可视化编辑面板(右下角浮动条) ----------
     功能:🖼️ 本页直接上传图片(不跳转原生上传页,传完显示文件名可直接填卡片)
           🎨 文字样式(字色/字号/字体可视化选择 → 源代码编辑器内直接包裹所选文字,
              可视化编辑器内则生成 {{彩色文字}}/{{字体样式}} 模板代码一键复制)
     仅 sysop(管理员)可见,访客不加载。                        */
  function isAdmin() {
    return (mw.config.get('wgUserGroups') || []).indexOf('sysop') !== -1;
  }
  /* 通用小弹窗:标题 + 内容HTML + 关闭 */
  function dyDialog(title, bodyHtml) {
    var ov = document.getElementById('dy-dlg');
    if (ov) ov.remove();
    ov = document.createElement('div');
    ov.id = 'dy-dlg';
    ov.innerHTML =
      '<div class="dy-dlg-card">' +
      '<div class="dy-dlg-bar"><span>' + title + '</span>' +
      '<button id="dy-dlg-close">✕</button></div>' +
      '<div class="dy-dlg-body">' + bodyHtml + '</div>' +
      '</div>';
    document.body.appendChild(ov);
    ov.addEventListener('click', function (e) { if (e.target === ov) ov.remove(); });
    ov.querySelector('#dy-dlg-close').addEventListener('click', function () { ov.remove(); });
    return ov;
  }
  /* 把代码写进源代码编辑器当前光标/选区(源代码编辑模式专用) */
  function insertToSourceEditor(code) {
    var ta = document.getElementById('wpTextbox1');
    if (!ta) return false;
    var s = ta.selectionStart, e = ta.selectionEnd;
    ta.value = ta.value.slice(0, s) + code + ta.value.slice(e);
    ta.selectionStart = ta.selectionEnd = s + code.length;
    ta.focus();
    return true;
  }
  /* 🎨 文字样式弹窗:颜色+字号+字体 → 编辑器包裹 / 模板代码 */
  function openTextDialog() {
    var sel = '';
    var ta = document.getElementById('wpTextbox1');
    if (ta && ta.selectionStart !== ta.selectionEnd) {
      sel = ta.value.slice(ta.selectionStart, ta.selectionEnd);
    }
    var colors = ['#b3261e', '#c0392b', '#e67e22', '#8a7434', '#2f7a3f', '#1f6f8b', '#3f51b5', '#7b1fa2', '#23281f', '#ffffff'];
    var sw = colors.map(function (c) {
      return '<button type="button" class="dy-sw" data-c="' + c + '" style="background:' + c + '"></button>';
    }).join('');
    var sizes = ['12px', '14px', '15px', '17px', '20px', '24px', '30px', '36px'].map(function (s) {
      return '<option value="' + s + '">' + s + '</option>';
    }).join('');
    var fonts = '<option value="">(不换字体)</option><option value="KaiTi">楷体</option>' +
      '<option value="SimSun">宋体</option><option value="SimHei">黑体</option>' +
      '<option value="FangSong">仿宋</option><option value="STZhongsong">中宋</option>';
    var ov = dyDialog('🎨 文字样式(字色 / 字号 / 字体)',
      '<div class="dy-frow"><label>文字</label><input id="dy-f-text" value="' + sel.replace(/"/g, '&quot;') + '" placeholder="要美化的文字"></div>' +
      '<div class="dy-frow"><label>颜色</label><div>' + sw +
      '<input type="color" id="dy-f-color" value="#b3261e" title="自定义颜色"></div></div>' +
      '<div class="dy-frow"><label>字号</label><select id="dy-f-size"><option value="">(默认大小)</option>' + sizes + '</select></div>' +
      '<div class="dy-frow"><label>字体</label><select id="dy-f-font">' + fonts + '</select></div>' +
      '<button id="dy-f-ok" class="dy-fbtn">应用到编辑器</button>' +
      '<div id="dy-f-tip" class="dy-ftip"></div>');
    // 色块点选 → 同步到取色器
    ov.querySelectorAll('.dy-sw').forEach(function (b) {
      b.addEventListener('click', function () { ov.querySelector('#dy-f-color').value = b.getAttribute('data-c'); });
    });
    ov.querySelector('#dy-f-ok').addEventListener('click', function () {
      var text = ov.querySelector('#dy-f-text').value || '文字';
      var color = ov.querySelector('#dy-f-color').value;
      var size = ov.querySelector('#dy-f-size').value;
      var font = ov.querySelector('#dy-f-font').value;
      var style = '';
      if (color) style += 'color:' + color + ';';
      if (size) style += 'font-size:' + size + ';';
      if (font) style += 'font-family:' + font + ';';
      if (!style) { alert('请至少选择一种样式'); return; }
      // 源代码编辑器:直接包裹所选文字,所见即所得
      if (document.getElementById('wpTextbox1')) {
        insertToSourceEditor('<span style="' + style + '">' + text + '</span>');
        ov.remove();
        return;
      }
      // ★ 可视化编辑器(VE):直接往编辑区插入带样式的文字(所见即所得),
      //   不再让用户去插模板——这是之前「字色切换没成功显示」的根因。
      //   两步法(实测最稳):① insertContent 插纯文本(选区自动覆盖新文字)
      //   ② annotateContent('set', 注解) 给选区套样式。
      //   注解必须由 ve.dm.annotationFactory.create 生成,不能用裸对象。
      var veSurface = window.ve && ve.init && ve.init.target && ve.init.target.surface;
      if (veSurface) {
        try {
          veSurface.getView().focus();                       // 聚焦编辑区,插入到当前光标
          var veModel = veSurface.getModel();
          var veFrag = veModel.getFragment();
          veFrag.insertContent(text);                        // ① 插入纯文本,选区自动落在文字上
          var veAnn = ve.dm.annotationFactory.create('textStyle/span',
            { attributes: { style: style } });
          veFrag.annotateContent('set', veAnn);              // ② 给选区加样式注解
          ov.remove();
          return;
        } catch (err) { /* VE 插入失败则走下面的模板代码兜底 */ }
      }
      // 普通页面(非编辑状态):生成模板代码 + 使用说明
      var wikitext = font
        ? '{{字体样式|字体=' + font + '|字号=' + (size || '') + '|文字=' + text + '}}'
        : '{{彩色文字|颜色=' + color + '|字号=' + (size || '') + '|文字=' + text + '}}';
      var tip = ov.querySelector('#dy-f-tip');
      tip.innerHTML = '请先进入编辑状态再点「🎨 文字样式」即可直接插入。<br>' +
        '生成代码(已复制):<code>' + wikitext.replace(/</g, '&lt;') + '</code>';
      try { navigator.clipboard.writeText(wikitext); } catch (e) {}
    });
  }
  /* 🖼️ 本页直接上传弹窗:选文件 → mw.Api 上传 → 显示文件名用法 */
  function openUploadDialog() {
    var ov = dyDialog('🖼️ 上传图片 / 头像(本页直传)',
      '<div class="dy-frow"><label>选择文件</label><input type="file" id="dy-u-file" accept="image/*"></div>' +
      '<div class="dy-frow"><label>文件名</label><input id="dy-u-name" placeholder="留空=自动用原文件名(建议英文/拼音)"></div>' +
      '<button id="dy-u-ok" class="dy-fbtn">开始上传</button>' +
      '<div id="dy-u-tip" class="dy-ftip">上传完成后会显示文件名;把文件名填进卡片模板的 头像= 参数即可换头像。</div>');
    ov.querySelector('#dy-u-ok').addEventListener('click', function () {
      var f = ov.querySelector('#dy-u-file').files[0];
      var tip = ov.querySelector('#dy-u-tip');
      if (!f) { alert('请先选择图片文件'); return; }
      var name = ov.querySelector('#dy-u-name').value.trim() || f.name;
      if (!/\.(png|jpe?g|gif|webp|svg)$/i.test(name)) {
        name += '.' + (f.name.split('.').pop() || 'jpg');     // 补扩展名
      }
      name = name.replace(/[^\w.-\u4e00-\u9fa5]/g, '_');      // 清理非法字符
      tip.textContent = '上传中…(' + name + ')';
      new mw.Api().upload(f, {
        filename: name,
        comment: '管理员页面直传(帝鹰维基)',
        ignorewarnings: true
      }).done(function (data) {
        var fn = data && data.upload && data.upload.filename ? data.upload.filename : name;
        tip.innerHTML = '✅ 上传成功!文件名:<b>' + fn + '</b><br>' +
          '用法:编辑主页卡片,把 头像= 参数改成 <code>' + fn + '</code> 即可;' +
          '文章里插入用 <code>[[' + fn + ']]</code>';
      }).fail(function (err) {
        tip.innerHTML = '❌ 上传失败:' + (err && err.error ? err.error.info : JSON.stringify(err));
      });
    });
  }
  /* 面板本体:右下角浮动条(替代旧「上传图片」跳转按钮) */
  function addAdminPanel() {
    if (!isAdmin()) return;                                 // 仅管理员可见
    if (document.getElementById('dy-admintool')) return;
    var bar = document.createElement('div');
    bar.id = 'dy-admintool';
    bar.innerHTML =
      '<span class="dy-at-cap">⚙️ 管理面板</span>' +
      '<button id="dy-at-upload">🖼️ 上传图片</button>' +
      '<button id="dy-at-text">🎨 文字样式</button>' +
      '<a id="dy-at-wiki" href="' + mw.util.getUrl('Special:Upload') + '" target="_blank" title="原生上传页(备用)">原生上传</a>';
    document.body.appendChild(bar);
    bar.querySelector('#dy-at-upload').addEventListener('click', openUploadDialog);
    bar.querySelector('#dy-at-text').addEventListener('click', openTextDialog);
  }

  /* ---------- ③ ♪ 帝鹰电台(侧边栏模块) ----------
     曲库来自 /music-list.js(服务器定时生成,window.DY_MUSIC 数组:
     [{id,title,src},...]),音频文件在本站 /music/ 目录,跨页不断播由
     浏览器自行决定;每个页面独立实例,点击播放即出声。          */
  function addRadio(panel, logo) {
    if (document.getElementById('dy-radio')) return;
    var box = document.createElement('div');
    box.id = 'dy-radio';
    box.innerHTML =
      '<div class="dy-mod-h">♪ 帝鹰电台</div>' +
      '<div id="dy-r-title">曲库加载中…</div>' +
      '<div id="dy-r-ctl">' +
      '<button data-act="prev" title="上一首">⏮</button>' +
      '<button data-act="play" title="播放/暂停">▶</button>' +
      '<button data-act="next" title="下一首">⏭</button>' +
      '</div>';
    if (logo && logo.nextSibling) panel.insertBefore(box, logo.nextSibling);
    else panel.appendChild(box);

    var list = [], idx = 0, audio = null, playing = false;
    var titleEl = box.querySelector('#dy-r-title');
    var playBtn = box.querySelector('[data-act="play"]');

    function ensureAudio() {
      if (audio) return;
      audio = new Audio();
      audio.addEventListener('ended', function () { next(1); });
      audio.addEventListener('playing', function () { playing = true; playBtn.textContent = '⏸'; });
      audio.addEventListener('pause', function () { playing = false; playBtn.textContent = '▶'; });
    }
    function show() {
      titleEl.textContent = list.length ? ('▶ ' + (idx + 1) + '. ' + list[idx].title) : '曲库为空';
    }
    function play() {
      if (!list.length) return;
      ensureAudio();
      var want = list[idx].src;
      if (audio.src !== location.origin + want && audio.getAttribute('src') !== want) {
        audio.src = want;                                   // 首次/切歌:设置源
      }
      if (playing) { audio.pause(); } else { audio.play(); }
    }
    function next(step) {
      if (!list.length) return;
      idx = (idx + step + list.length) % list.length;
      if (audio) { audio.src = list[idx].src; audio.play(); }
      show();
    }
    box.addEventListener('click', function (e) {
      var act = e.target.getAttribute && e.target.getAttribute('data-act');
      if (act === 'play') play();
      else if (act === 'next') next(1);
      else if (act === 'prev') next(-1);
    });
    // 曲库脚本:/music-list.js 内容为 window.DY_MUSIC=[...]
    var s = document.createElement('script');
    s.src = '/music-list.js';
    s.onload = function () {
      list = (window.DY_MUSIC || []).filter(function (m) { return m && m.src; });
      show();
    };
    s.onerror = function () { titleEl.textContent = '电台曲库暂不可用'; };
    document.head.appendChild(s);
  }

  /* ---------- ④ 💬 帝鹰聊天室(侧边栏入口 + 全屏弹层) ----------
     后端 = 同站 /chat/(api.php + index.html 自托管聊天室)。
     主内容区不放聊天室,只有侧边栏这一个入口。               */
  function addChat(panel, radio) {
    if (document.getElementById('dy-chat')) return;
    var box = document.createElement('div');
    box.id = 'dy-chat';
    box.innerHTML =
      '<div class="dy-mod-h">💬 帝鹰聊天室</div>' +
      '<button id="dy-chat-open">进入聊天室</button>';
    if (radio && radio.nextSibling) panel.insertBefore(box, radio.nextSibling);
    else panel.appendChild(box);

    box.querySelector('#dy-chat-open').addEventListener('click', function () {
      var ov = document.getElementById('dy-chat-ov');
      if (!ov) {
        ov = document.createElement('div');
        ov.id = 'dy-chat-ov';
        ov.innerHTML =
          '<div class="dy-chat-frame">' +
          '<div class="dy-chat-bar"><span>帝鹰聊天室</span>' +
          '<button id="dy-chat-close">✕ 关闭</button></div>' +
          '<iframe src="/chat/index.html" title="帝鹰聊天室"></iframe>' +
          '</div>';
        document.body.appendChild(ov);
        ov.querySelector('#dy-chat-close').addEventListener('click', function () { ov.hidden = true; });
        ov.addEventListener('click', function (e) { if (e.target === ov) ov.hidden = true; }); // 点空白处也可关
      }
      ov.hidden = false;
    });
  }

  /* ---------- ⑤ 📞 联系入口(侧边栏模块):联系窗口 + 🤖 AI 咨询窗口 ---------- */
  function addContact(panel, chatBox) {
    if (document.getElementById('dy-contact')) return;
    var box = document.createElement('div');
    box.id = 'dy-contact';
    box.innerHTML =
      '<div class="dy-mod-h">📞 联系入口</div>' +
      '<button id="dy-contact-open">📞 联系窗口</button>' +
      '<button id="dy-ai-open">🤖 咨询窗口</button>';
    if (chatBox && chatBox.nextSibling) panel.insertBefore(box, chatBox.nextSibling);
    else panel.appendChild(box);

    /* 联系窗口:管理事务通过管理员账号登录后台处理;即时交流走聊天室 */
    box.querySelector('#dy-contact-open').addEventListener('click', function () {
      var ov = dyDialog('📞 联系窗口',
        '<div class="dy-ct-item"><b>站务/词条管理:</b>管理员账号密码登录本站即可直接处理</div>' +
        '<div class="dy-ct-item"><b>即时交流:</b>帝鹰聊天室(全员实时在线)</div>' +
        '<div class="dy-ct-item"><b>咨询求助:</b>侧边栏「🤖 咨询窗口」AI 助手随时解答</div>' +
        '<button id="dy-ct-chat" class="dy-fbtn">💬 前往帝鹰聊天室</button>');
      ov.querySelector('#dy-ct-chat').addEventListener('click', function () {
        ov.remove();
        var btn = document.getElementById('dy-chat-open');
        if (btn) btn.click();
      });
    });

    /* 咨询窗口:AI 对话(后端 /ai.php,密钥在服务端) */
    box.querySelector('#dy-ai-open').addEventListener('click', function () {
      var ov = dyDialog('🤖 咨询窗口 · 帝鹰维基 AI 助手',
        '<div id="dy-ai-log">' +
        '<div class="dy-ai-msg bot">你好!我是帝鹰维基咨询助手 🦅<br>可以问我:词条在哪找 / 怎么入会 / 本站怎么用…</div>' +
        '</div>' +
        '<div class="dy-ai-input"><input id="dy-ai-text" placeholder="输入你的问题…">' +
        '<button id="dy-ai-send">发送</button></div>');
      var log = ov.querySelector('#dy-ai-log');
      var input = ov.querySelector('#dy-ai-text');
      var history = [];
      function bubble(cls, text) {
        var d = document.createElement('div');
        d.className = 'dy-ai-msg ' + cls;
        d.textContent = text;
        log.appendChild(d);
        log.scrollTop = log.scrollHeight;
        return d;
      }
      function send() {
        var text = input.value.trim();
        if (!text) return;
        input.value = '';
        bubble('user', text);
        var loading = bubble('bot', '思考中…');
        fetch('/ai.php', { method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ message: text, history: history }) })
          .then(function (r) { return r.json(); })
          .then(function (j) {
            loading.remove();
            var reply = j && j.ok ? (j.reply || '(AI 未返回内容)') : ('出错了:' + (j && j.error || '未知错误'));
            bubble('bot', reply);
            history.push({ role: 'user', content: text }, { role: 'assistant', content: reply });
            if (history.length > 16) history = history.slice(-16);   // 只保留最近 8 轮
          })
          .catch(function (e) { loading.remove(); bubble('bot', '网络异常:' + e.message); });
      }
      ov.querySelector('#dy-ai-send').addEventListener('click', send);
      input.addEventListener('keydown', function (e) { if (e.key === 'Enter') send(); });
      setTimeout(function () { input.focus(); }, 100);
    });
  }

  /* ---------- 装配入口 ---------- */
  function buildSide(panel) {
    var logo = document.getElementById('p-logo');
    addRadio(panel, logo);
    addChat(panel, document.getElementById('dy-radio'));
    addContact(panel, document.getElementById('dy-chat'));
  }
  function boot() {
    buildNav();
    addAdminPanel();
    var panel = document.getElementById('mw-panel');
    if (panel) buildSide(panel);
  }
  if (window.jQuery && mw && mw.util) {
    $(boot);
  } else {
    document.addEventListener('DOMContentLoaded', boot);
  }
})();