MediaWiki:Common.js

来自虾仁维基
跳转到导航 跳转到搜索

注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5
/* 虾仁维基 - 自定义侧边栏导航 */
/* 作者: 虾仁维基团队 */
/* 最后更新: 2026-07-26 */

(function($, mw) {
    'use strict';

    var COLLAPSE_KEY = 'xiarenwiki-nav-v2';
    var WORKS = ['我在大明那些年', '彼岸轮回', '一品布衣'];
    var WORK_DIRECT = ['宇宙层', '人物', '世界观', '设定'];

    function init() {
        if (!mw.config.get('wgIsArticle') && mw.config.get('wgCanonicalSpecialPageName')) {
            return;
        }
        waitForPanel(function($panel) {
            buildNav($panel);
        });
    }

    function waitForPanel(cb) {
        var $panel = $('#mw-panel');
        if ($panel.length) return cb($panel);
        var tries = 0;
        var timer = setInterval(function() {
            $panel = $('#mw-panel');
            if ($panel.length || ++tries > 50) {
                clearInterval(timer);
                if ($panel.length) cb($panel);
            }
        }, 100);
    }

    /* ========== BUILD ========== */

    function buildNav($panel) {
        var html = '';

                // 搜索框
        html += '<div class="xn-search">';
        html += '<form action="' + mw.util.getUrl('Special:Search') + '" method="get">';
        html += '<input type="text" name="search" class="xn-search-input" placeholder="搜索虾仁维基..." />';
        html += '</form></div>';

        html += section('Wiki', [
            ['首页', '首页'],
            ['Wiki简介', 'Wiki简介'],
            ['管理员告示', '管理员告示'],
            ['随机页面', 'Special:Random'],
            ['快速搜索', 'Special:Search'],
            ['相关平台', '相关平台'],
            ['Wiki源站点', 'Wiki源站点']
        ]);

        html += section('虾仁简介', [
            ['虾仁?', '虾仁?'],
            ['虾仁共创收录规则', '虾仁共创收录规则'],
            ['虾仁宇宙结构介绍', '虾仁宇宙结构介绍']
        ]);

        html += '<div class="xn-section">';
        html += '<div class="xn-header xn-expandable" data-sid="universe">虾仁宇宙</div>';
        html += '<div class="xn-body" id="xn-universe"><div class="xn-loading">加载中...</div></div>';
        html += '</div>';

        html += '<div class="xn-section">';
        html += '<div class="xn-header xn-expandable" data-sid="literature">虾仁文学</div>';
        html += '<div class="xn-body" id="xn-literature">';
        html += link('创始宣言', '创始宣言');
        html += link('虾仁六章', '虾仁六章');
        html += link('虾仁异志', '虾仁异志');
        html += link('虾仁诗作', '虾仁诗作');
        html += link('虾仁音乐', '虾仁音乐');
        html += '<div class="xn-subsection">';
        html += '<div class="xn-subheader xn-expandable" data-sid="papers">虾仁论文 ' + plus('虾仁论文') + '</div>';
        html += '<div class="xn-subbody" id="xn-papers">';
        html += link('去中心化创作模式的研究', '去中心化创作模式的研究');
        html += link('数字时代的孙悟空', '数字时代的孙悟空');
        html += '</div></div></div></div>';

        html += section('虾仁历史', [
            ['起源', '虾仁历史/起源'],
            ['传播', '虾仁历史/传播'],
            ['转折', '虾仁历史/转折'],
            ['爆发', '虾仁历史/爆发'],
            ['危机', '虾仁历史/危机'],
            ['转型', '虾仁历史/转型']
        ]);

        html += '<div class="xn-single"><a href="' + url('共创入口') + '">共创入口</a></div>';

        $panel.html(html);
        restoreCollapse();
        bindEvents();
        highlightCurrent();
        loadUniverse();
    }

    /* ========== HTML HELPERS ========== */

    function section(title, items) {
        var sid = title.replace(/[^a-z0-9\u4e00-\u9fff]/gi, '').toLowerCase();
        var h = '<div class="xn-section">';
        h += '<div class="xn-header xn-expandable" data-sid="' + sid + '">' + title + '</div>';
        h += '<div class="xn-body" id="xn-' + sid + '">';
        for (var i = 0; i < items.length; i++) {
            h += link(items[i][0], items[i][1]);
        }
        h += '</div></div>';
        return h;
    }

    function link(label, page, cls) {
        return '<div class="xn-link' + (cls ? ' ' + cls : '') + '"><a href="' + url(page) + '">' + label + '</a></div>';
    }

    function url(page) { return mw.util.getUrl(page); }

    function plus(page) {
        return '<a class="xn-plus" href="' + url(page) + '?action=edit" title="编辑/新增">+</a>';
    }

    /* ========== UNIVERSE DYNAMIC LOAD ========== */

    async function loadUniverse() {
        var $body = $('#xn-universe');
        if (!$body.length) return;

        try {
            var api = new mw.Api();
            var html = '';

            for (var i = 0; i < WORKS.length; i++) {
                var work = WORKS[i];
                var workSid = 'work' + i;

                var subpages = [];
                try {
                    var result = await api.get({
                        action: 'query',
                        list: 'allpages',
                        apprefix: work + '/',
                        aplimit: 100
                    });
                    subpages = result.query.allpages || [];
                } catch(e) {
                    console.error('API error for ' + work, e);
                }

                html += '<div class="xn-work">';
                html += '<div class="xn-header xn-expandable" data-sid="' + workSid + '">' + work + '</div>';
                html += '<div class="xn-body" id="xn-' + workSid + '">';

                // Direct subpages under work
                var directSet = {};
                for (var j = 0; j < subpages.length; j++) {
                    var rel = subpages[j].title.substring(work.length + 1);
                    if (rel.indexOf('/') === -1) directSet[rel] = true;
                }

                for (var k = 0; k < WORK_DIRECT.length; k++) {
                    var item = WORK_DIRECT[k];
                    var exists = directSet[item];
                    html += link(item, work + '/' + item, exists ? '' : 'xn-missing');
                }

                // 剧情层
                var plotSegs = [];
                for (j = 0; j < subpages.length; j++) {
                    rel = subpages[j].title.substring(work.length + 1);
                    var parts = rel.split('/');
                    if (parts.length === 2 && parts[0] === '剧情层') {
                        plotSegs.push(parts[1]);
                    }
                }

                var plotSid = workSid + 'plot';
                html += '<div class="xn-subsection">';
                html += '<div class="xn-subheader xn-expandable" data-sid="' + plotSid + '">剧情层 ' + plus(work + '/剧情层') + '</div>';
                html += '<div class="xn-subbody" id="xn-' + plotSid + '">';

                for (k = 0; k < plotSegs.length; k++) {
                    var seg = plotSegs[k];
                    var segSid = workSid + 'seg' + k;
                    var segPage = work + '/剧情层/' + seg;

                    // Fetch 金集 for this segment
                    var jinji = [];
                    try {
                        var segResult = await api.get({
                            action: 'query',
                            list: 'allpages',
                            apprefix: segPage + '/',
                            aplimit: 50
                        });
                        var segPages = segResult.query.allpages || [];
                        for (var m = 0; m < segPages.length; m++) {
                            jinji.push(segPages[m].title.substring(segPage.length + 1));
                        }
                    } catch(e2) {}

                    html += '<div class="xn-subsubsection">';
                    html += '<div class="xn-subsubheader xn-expandable" data-sid="' + segSid + '">' + seg + ' ' + plus(segPage) + '</div>';
                    html += '<div class="xn-subsubbody" id="xn-' + segSid + '">';

                    for (m = 0; m < jinji.length; m++) {
                        html += link(jinji[m], segPage + '/' + jinji[m]);
                    }

                    html += '</div></div>';
                }

                html += '</div></div>'; // close 剧情层
                html += '</div></div>'; // close work
            }

            // Add new work link
            html += '<div class="xn-link xn-add-work"><a href="' + url('Special:Search') + '" title="搜索已有作品">+ 发现更多作品</a></div>';

            $body.html(html);
            restoreCollapse();
            bindEvents();
            highlightCurrent();

        } catch(e) {
            console.error('Failed to load universe nav', e);
            $body.html('<div class="xn-link">(加载失败,请刷新)</div>');
        }
    }


    /* ========== CURRENT PAGE HIGHLIGHT ========== */
    function highlightCurrent() {
        var currentTitle = mw.config.get('wgPageName');
        $('#mw-panel .xn-link a').each(function() {
            var href = $(this).attr('href');
            if (!href) return;
            var match = false;
            if (href.indexOf('title=') > 0) {
                var m = href.match(/title=([^&]+)/);
                if (m) match = decodeURIComponent(m[1]) === currentTitle;
            } else {
                var parts = href.split('/');
                var last = parts[parts.length - 1];
                match = decodeURIComponent(last) === currentTitle;
            }
            if (match) {
                $(this).addClass('xn-active');
                $(this).closest('.xn-link').addClass('xn-active-link');
                $(this).parents('.xn-body, .xn-subbody, .xn-subsubbody').each(function() {
                    $(this).show();
                    var sid = $(this).attr('id');
                    if (sid && sid.indexOf('xn-') === 0) {
                        sid = sid.substring(3);
                        $('[data-sid="' + sid + '"]').removeClass('xn-collapsed');
                    }
                });
            }
        });
    }
    /* ========== EVENTS & STATE ========== */

    function bindEvents() {
        $(document).off('click.xn').on('click.xn', '.xn-expandable', function(e) {
            if ($(e.target).closest('.xn-plus').length) return; // don't toggle when clicking [+]
            var sid = $(this).data('sid');
            var $body = $('#xn-' + sid);
            if (!$body.length) $body = $(this).next('.xn-body,.xn-subbody,.xn-subsubbody');
            if (!$body.length) return;

            $body.slideToggle(150);
            $(this).toggleClass('xn-collapsed', !$body.is(':visible'));

            var state = loadState();
            state[sid] = $body.is(':visible');
            saveState(state);
        });
    }

    function restoreCollapse() {
        var state = loadState();
        var defaults = { 'universe': true };
        for (var key in defaults) {
            if (!(key in state)) state[key] = defaults[key];
        }

        Object.keys(state).forEach(function(sid) {
            var $header = $('[data-sid="' + sid + '"]');
            var $body = $('#xn-' + sid);
            if (!$body.length) $body = $header.next('.xn-body,.xn-subbody,.xn-subsubbody');
            if (!$body.length) return;

            if (state[sid]) {
                $body.show();
                $header.removeClass('xn-collapsed');
            } else {
                $body.hide();
                $header.addClass('xn-collapsed');
            }
        });
    }

    function loadState() {
        try { return JSON.parse(localStorage.getItem(COLLAPSE_KEY) || '{}'); }
        catch(e) { return {}; }
    }
    function saveState(s) {
        try { localStorage.setItem(COLLAPSE_KEY, JSON.stringify(s)); }
        catch(e) {}
    }

    /* ========== START ========== */

    mw.loader.using('mediawiki.api').then(function() {
        $(init);
    });

})(jQuery, mediaWiki);