这是林泊百科的迁移技术测试站点,所有改动不会同步到林泊百科上,此站点将会在正式迁移后关闭,请勿存放重要数据

MediaWiki:密码盘.js

来自Limbo Wiki
晨茗biu~留言 | 贡献2026年8月19日 (三) 21:13的版本 (新功能添加)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)

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

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5
(function ($) {
  'use strict';

  /* 逗号分隔串 -> 数组 */
  function splitList(str) {
    if (!str) { return []; }
    var out = [];
    $.each(String(str).split(','), function (_, s) {
      var v = $.trim(s);
      if (v) { out.push(v); }
    });
    return out;
  }

  /* 解析 correct */
  function parseAnswers(str) {
    if (!str) { return []; }
    var groups = [];
    $.each(String(str).split(';'), function (_, part) {
      var p = $.trim(part);
      if (!p) { return; }
      var list = splitList(p);
      if (list.length) { groups.push(list); }
    });
    return groups;
  }

  /* 解析 target:与 correct 的组一一对应 */
  function parseTargets(str) {
    if (!str) { return []; }
    var groups = [];
    $.each(String(str).split(';'), function (_, part) {
      var p = $.trim(part);
      if (!p) { return; }
      var list = splitList(p);
      if (list.length) { groups.push(list); }
    });
    return groups;
  }

  /* 解析 layout;无效返回 null */
  function parseLayout(str) {
    if (!str) { return null; }
    var out = [];
    $.each(String(str).split(','), function (_, s) {
      var n = parseInt($.trim(s), 10);
      if (!isNaN(n) && n > 0) { out.push(n); }
    });
    return out.length ? out : null;
  }

  /* 键名消毒,防注入选择器 */
  function normalizeKey(k) {
    return String(k || '').replace(/[^A-Za-z0-9_-]/g, '');
  }

  /* 开关参数判定:1 / true / yes / on 视为启用,其余视为关闭 */
  function isEnabled(val) {
    if (!val) { return false; }
    var v = $.trim(String(val)).toLowerCase();
    return v === '1' || v === 'true' || v === 'yes' || v === 'on';
  }

  function setup(pad) {
    var $pad = $(pad);
    if ($pad.data('pdInit')) { return; }
    $pad.data('pdInit', true);

    var answerGroups = parseAnswers($pad.attr('data-answer'));
    var targetGroups = parseTargets($pad.attr('data-target'));
    var $msg = $pad.find('.pd-msg');

    /* 自动隐藏开关 */
    var autoHide = isEnabled($pad.attr('data-autohide'));

    /* 提示文案:取模板内已按 wikitext 解析好的 span 内容,缺省回退默认 */
    var $okEl = $pad.find('.pd-okmsg');
    var $errEl = $pad.find('.pd-errmsg');
    var okMsg = $okEl.length ? $okEl.html() : '✓ 密码正确';
    var errMsg = $errEl.length ? $errEl.html() : '✗ 密码错误,请重试';

    if (!answerGroups.length) {
      $msg.text('未配置密码:请设置 correct 参数').attr('class', 'pd-msg pd-msg-err');
      return;
    }

    /* 位数:缺省取第一个密码长度;要求所有密码位数一致 */
    var length = parseInt($pad.attr('data-length'), 10);
    if (isNaN(length) || length < 1) { length = answerGroups[0].length; }
    for (var g = 0; g < answerGroups.length; g++) {
      if (answerGroups[g].length !== length) {
        $msg.text('配置错误:各密码位数不一致').attr('class', 'pd-msg pd-msg-err');
        return;
      }
    }

    /* 符号:用户未填 symbols 时,默认 QWERTY 键盘 26 个大写字母 */
    var symbolsRaw = $.trim($pad.attr('data-symbols') || '');
    var symbolsProvided = symbolsRaw !== '';
    var symbols = splitList(symbolsRaw);
    if (!symbols.length) {
      symbols = [
        'Q','W','E','R','T','Y','U','I','O','P',
        'A','S','D','F','G','H','J','K','L',
        'Z','X','C','V','B','N','M'
      ];
    }

    /* 布局:显式填写 layout 优先;未填时——默认键盘用 QWERTY 三行 [10,9,7],自定义符号用单行 */
    var layout = parseLayout($pad.attr('data-layout'));
    if (!layout && !symbolsProvided) {
      layout = [10, 9, 7];
    }

    var input = [];
    var timer = null;

    var $display = $pad.find('.pd-display');
    var $keys = $pad.find('.pd-keys');

    /* 槽位 */
    $display.empty();
    var slots = [];
    for (var i = 0; i < length; i++) {
      slots.push($('<span class="pd-slot"></span>').appendTo($display));
    }

    /* 按键按 layout 分行 */
    var rows = [];
    if (layout) {
      var idx = 0;
      for (var r = 0; r < layout.length; r++) {
        var row = [];
        for (var c = 0; c < layout[r]; c++) {
          if (idx < symbols.length) { row.push(symbols[idx]); idx++; }
        }
        rows.push(row);
      }
      while (idx < symbols.length) { rows[rows.length - 1].push(symbols[idx]); idx++; }
    } else {
      rows.push(symbols.slice());
    }

    /* 创建单个按键 */
    function addKey($row, sym) {
      $('<button type="button" class="pd-key"></button>')
        .text(sym)
        .on('click', function () { press(sym); })
        .appendTo($row);
    }

    /* 生成按键 */
    $keys.empty();
    for (r = 0; r < rows.length; r++) {
      var $row = $('<div class="pd-row"></div>').appendTo($keys);
      for (c = 0; c < rows[r].length; c++) {
        addKey($row, rows[r][c]);
      }
    }

    /* 控制行:退格 + 清空 */
    var $ctrl = $('<div class="pd-row pd-ctrl-row"></div>').appendTo($keys);
    $('<button type="button" class="pd-key pd-ctl" title="退格(删除上一个符号)">⌫</button>')
      .on('click', backspace).appendTo($ctrl);
    $('<button type="button" class="pd-key pd-ctl" title="清空全部输入">清空</button>')
      .on('click', clearAll).appendTo($ctrl);

    function render() {
      for (var j = 0; j < slots.length; j++) {
        if (j < input.length) {
          slots[j].text(input[j]).addClass('pd-slot-filled');
        } else {
          slots[j].text('').removeClass('pd-slot-filled');
        }
      }
    }
    function setMsg(text, kind) {
      $msg.html(text || '').attr('class', 'pd-msg' + (kind ? ' pd-msg-' + kind : ''));
    }

    /* 解锁第 groupIdx 组目标:限定在页面内容区内查找 .pd-secret-<键> */
    function revealTargets(groupIdx) {
      if (groupIdx < 0 || groupIdx >= targetGroups.length) { return; }
      var keys = targetGroups[groupIdx];
      if (!keys.length) { return; }
      var $scope = $pad.closest('.mw-parser-output').first();
      if (!$scope.length) { $scope = $(document); }
      for (var k = 0; k < keys.length; k++) {
        var key = normalizeKey(keys[k]);
        if (!key) { continue; }
        $scope.find('.pd-secret-' + key)
          .removeClass('pd-secret-locked')
          .addClass('pd-secret-open')
          .css('display', '');
      }
    }

    /* 自动隐藏:隐藏本密码盘 target 范围内、不属于本次解锁组的其他已解锁内容 */
    function hideOtherTargets(currentIdx) {
      var $scope = $pad.closest('.mw-parser-output').first();
      if (!$scope.length) { $scope = $(document); }
      /* 本次解锁组内的键保持显示(含组间共享的键) */
      var keep = {};
      var cur = targetGroups[currentIdx] || [];
      for (var kk = 0; kk < cur.length; kk++) {
        var ck = normalizeKey(cur[kk]);
        if (ck) { keep[ck] = true; }
      }
      for (var gi = 0; gi < targetGroups.length; gi++) {
        if (gi === currentIdx) { continue; }
        var keys = targetGroups[gi];
        for (var k = 0; k < keys.length; k++) {
          var key = normalizeKey(keys[k]);
          if (!key || keep[key]) { continue; }
          $scope.find('.pd-secret-' + key)
            .removeClass('pd-secret-open')
            .addClass('pd-secret-locked')
            .css('display', 'none');
        }
      }
    }

    function press(sym) {
      /* 取消待执行的自动清空,避免误清新输入 */
      if (timer) { clearTimeout(timer); timer = null; $pad.removeClass('pd-shake'); }
      if (input.length >= length) { return; }
      input.push(sym);
      render();
      /* 不在此清空消息,提示保留到下一次判定 */
      if (input.length === length) { check(); }
    }

    /* 查找与当前输入完全一致的密码组,返回组下标;无则 -1 */
    function findMatch() {
      for (var gi = 0; gi < answerGroups.length; gi++) {
        if (answerGroups[gi].length !== input.length) { continue; }
        var ok = true;
        for (var k = 0; k < answerGroups[gi].length; k++) {
          if (input[k] !== answerGroups[gi][k]) { ok = false; break; }
        }
        if (ok) { return gi; }
      }
      return -1;
    }

    function check() {
      var hit = findMatch();
      if (hit >= 0) {
        /* 命中:解锁对应内容,不锁定;自动清空输入以便继续,但消息保留 */
        $pad.addClass('pd-open');
        revealTargets(hit);
        if (autoHide) { hideOtherTargets(hit); }
        setMsg(okMsg, 'ok');
        timer = setTimeout(function () {
          input = [];
          render();
        }, 800);
      } else {
        setMsg(errMsg, 'err');
        $pad.addClass('pd-shake');
        timer = setTimeout(function () {
          $pad.removeClass('pd-shake');
          input = [];
          render();
        }, 1000);
      }
    }

    function backspace() {
      if (timer) { clearTimeout(timer); timer = null; $pad.removeClass('pd-shake'); }
      if (input.length > 0) { input.pop(); render(); }
    }

    function clearAll() {
      if (timer) { clearTimeout(timer); timer = null; $pad.removeClass('pd-shake'); }
      input = [];
      render();
    }

    render();
  }

  function init($root) {
    ($root || $(document)).find('.pd-pad').each(function () { setup(this); });
  }

  /* 初始化:DOM 就绪 + wikipage.content 钩子(兼容动态加载内容) */
  $(function () { init($(document)); });
  if (window.mw && mw.hook) { mw.hook('wikipage.content').add(function ($c) { init($c); }); }
})(jQuery);