置顶公告:【置顶】关于临时开启评论区所有功能的公告(2022.10.22) | 【置顶】关于本站Widget恢复使用的公告
  • 你好~!欢迎来到萌娘百科镜像站!如需查看或编辑,请联系本站管理员注册账号。
  • 本镜像站和其他萌娘百科的镜像站无关,请注意分别。

User:BearBin/js/BulkMove.js

猛汉♂百科,万男皆可猛的百科全书!转载请标注来源页面的网页链接,并声明引自猛汉百科。内容不可商用。
跳到导航 跳到搜索

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

  • Firefox/Safari:按住“Shift”的同时单击“刷新”,或按“Ctrl-F5”或“Ctrl-R”(Mac为“⌘-R”)
  • Google Chrome:按“Ctrl-Shift-R”(Mac为“⌘-Shift-R”)
  • Internet Explorer:按住“Ctrl”的同时单击“刷新”,或按“Ctrl-F5”
  • Opera:在“工具→首选项”中清除缓存
如果您已登录但该页面出现未登录状态,请尝试在地址栏的地址最后添加代码?_=1来访问最新页面。
添加代码后的本页地址如下:-{R|https://moegirl.uk/User:BearBin/js/BulkMove.js?_=1}-
文档  [编辑]

本小工具用于批量移动页面。

使用方式:在个人js页添加如下代码:

mw.loader.load("/index.php?title=User:BearBin/js/BulkMove.js&action=raw&ctype=text/javascript");
// eslint-disable-next-line
var _addText = '{{Documentation|content=本小工具用于批量移动页面。\n\n使用方式:在[[Special:MyPage/common.js|个人js页]]添加如下代码:\n<pre class="prettyprint lang-javascript" style="margin-top:0">mw.loader.load("/index.php?title=User:BearBin/js/BulkMove.js&action=raw&ctype=text/javascript");</pre></pre>}}';

"use strict";
// <pre>
$(() => (async () => {
    if (!["special:bulkmove", "special:批量移动", "special:批量移動", "特殊:bulkmove"].includes(mw.config.get("wgPageName").toLowerCase())) {
        await mw.loader.using(["mediawiki.util"]);
        mw.util.addPortletLink("p-tb", "/Special:BulkMove", "批量移动", "t-bulkmove");
    } else {
        await mw.loader.using(["mediawiki.api", "oojs-ui", "mediawiki.user"]);
        const api = new mw.Api();
        let rowCount = 0;
        let successCount = 0;
        let errorCount = 0;

        /**
         * 实现sleep效果,使用时需要加上await
         * @param {number} time 等待时间(ms)
         * @returns 
         */
        const waitInterval = (time) => new Promise((resolve) => setTimeout(resolve, time));

        /**
         * 添加表格行
         * @param {number} count 行数
         */
        const addRow = (count = 1) => {
            for (let i = 0; i < count; i++) {
                rowCount++;
                $("#bm-page-list-table tbody").append($("<tr>")
                    .append($(`<td><input type="text" data-row-no="${rowCount}" data-col-no="1"></td>`))
                    .append($(`<td><input type="text" data-row-no="${rowCount}" data-col-no="2"></td>`))
                    .append($('<td><div class="remove-row oo-ui-icon-subtract" title="删除此行"></div></td>')));
            }
        };

        /**
         * 记录日志
         * @param {string} info 日志内容
         * @param {string} type 日志类型,normal/success/warn/error
         */
        const record = (info, type = "normal") => {
            $("#bm-log-lines").append(`<li class="log-${type}">${new Date().toLocaleTimeString()} - ${info}</li>`);
            const message = document.getElementById("bm-log-lines");
            message.scrollTop = message.scrollHeight;
        };

        /**
         * 在Special:BulkMove构建页面
         */
        mw.config.set("wgCanonicalSpecialPageName", "BulkMove");
        $("title").text("批量移动 - 萌娘百科_万物皆可萌的百科全书");
        $("head").append(`<style>
            #firstHeading {
                display: flex;
                flex-wrap: wrap;
                justify-content: space-between;
                align-items: flex-end;
            }
            #firstHeading>div {
                font-size: .6em;
            }
            #bodyContent {
                padding-right: 0 !important;
            }
            #mw-content-text h3 {
                margin-bottom: 0;
            }
            #bm-page-list-table {
                border-collapse: collapse;
                width: 100%;
                max-width: 900px;
            }
            #bm-page-list-table tbody {
                counter-reset: row-counter;
            }
            #bm-page-list-table tbody tr {
                counter-increment: row-counter;
            }
            #bm-page-list-table tbody tr:before {
                content: counter(row-counter);
                display: table-cell;
                width: 0;
                padding-right: .3em;
                text-align: center;
                vertical-align: middle;
            }
            #bm-page-list-table td {
                padding: 0;
                border: 1px solid #aaa;
                background-color: rgba(255, 255, 255, .7);
            }
            #bm-page-list-table th:last-child,
            #bm-page-list-table td:last-child {
                border: none;
                width: 20px;
                background-position: center;
                background-color: transparent;
            }
            #bm-add-row, .remove-row {
                width: 20px;
                height: 20px;
                cursor: pointer;
                border-radius: 50%;
                transition: .2s ease background;
            }
            #bm-add-row:hover, .remove-row:hover {
                background-color: rgba(127, 127, 127, .2)
            }
            /* 傻逼萌皮 */
            #mw-content-text #bm-page-list-table input[type="text"] {
                box-sizing: border-box;
                width: 100%;
                height: 100%;
                padding: .4em;
                border: none;
                border-radius: 0;
                background: transparent;
            }
            #mw-content-text #bm-page-list-table input[type="text"]:focus {
                outline: none;
                background-color: rgba(127, 127, 127, .02);
            }
            #mw-content-text ul.bm-note {
                margin: .4em 0 0 1.6em;
            }
            #bm-submit-panel, #bm-option {
                margin-top: .8em;
            }
            #bm-submit-panel {
                display: flex;
                gap: .1rem;
            }
            #bm-interval {
                flex: 0 0 5.5em;
            }
            #bm-summary {
                max-width: initial;
            }
            #bm-option>div {
                margin-top: .3em;
            }
            .state {
                display: inline-block;
                width: 1.2em;
                height: 1em;
                line-height: 1em;
                text-align: center;
            }
            #bm-log {
                display: flow-root;
                padding: .3em;
                border: 1px solid #ccc;
                background: rgba(255, 255, 255, .7);
            }
            #bm-log-clear {
                padding-left: .5em;
                font-size: 1rem;
                font-weight: 400;
                user-select: none;
            }
            #bm-log-state {
                float: right;
                padding: .4em;
            }
            #bm-log-state>div {
                border-radius: 0.3em;
                margin-bottom: 0.2em;
                padding-right: 0.2em;
                cursor: pointer;
            }
            #bm-log-state>div.log-selected {
                background-color: rgba(127, 127, 127, .07);
            }
            #bm-log-lines.log-success-hide .log-success,
            #bm-log-lines.log-nochange-hide .log-nochange,
            #bm-log-lines.log-error-hide .log-error,
            #bm-log-lines.log-warn-hide .log-warn {
                display: none;
            }
            .log-success {
                color: #333;
            }
            .log-nochange {
                color: #888;
            }
            .log-warn {
                color: #f28500;
            }
            .log-error {
                color: #eb3941;
            }
            #bm-log-lines {
                font-family: monospace;
            }
            #bm-log-lines a {
                color: inherit;
                text-decoration: underline dotted;
            }
            </style>`);
        $(".mw-invalidspecialpage").removeClass("mw-invalidspecialpage");
        $("#firstHeading").html("批量移动页面<div>By BearBin</div>");
        $("#contentSub").remove();
        $("#mw-content-text").html(`
            <p>使用此工具操作时请注意<a href="/萌娘百科:机器用户">机器用户方针</a>所规定的速率,必要时请联系管理员申请机器用户。</p>
            <h3>页面列表</h3>
            <table id="bm-page-list-table">
                <thead>
                    <tr>
                        <th></th>
                        <th>源页面</th>
                        <th>目标页面</th>
                        <th><div class="oo-ui-icon-add" id="bm-add-row" title="新增一行"></div></th>
                    </tr>
                </thead>
                <tbody></tbody>
            </table>
            <ul class="bm-note">
                <li>请输入要移动的页面列表及目标页面,一一对应。</li>
                <li>可直接从Excel复制(部分浏览器可能不支持)。复制粘贴时浏览器可能会需要获取权限,请注意是否有提醒。</li>
            </ul>
            <div id="bm-option"></div>
            <div id="bm-submit-panel"></div>
            <ul class="bm-note">
                <li>操作间隔单位为毫秒(ms),不填默认为0。不包含本身移动页面所用的服务器响应时间。</li>
                <li>非维护人员请注意<a target="_blank" href="/api.php?action=query&meta=userinfo&uiprop=ratelimits">ratelimit限制</a>,自行设置间隔以免撞墙。</li>
            </ul>
            <h3 id="bm-log-title">日志<a id="bm-log-clear">[清空]</a></h3>
            <div id="bm-log">
                <div id="bm-log-state">
                    <div class="log-success log-selected" id="log-success"><span class="state">✓</span><span id="state-success">0</span> 完成</div>
                    <div class="log-error log-selected" id="log-error"><span class="state">✕</span><span id="state-error">0</span> 出错</div>
                    <div class="log-warn log-selected" id="log-warn"><span class="state">!</span><span id="state-error">0</span> 警告</div>
                </div>
                <ul id="bm-log-lines"></ul>
            </div>
        `);
        addRow(10); // 先加十行

        // 移动选项
        const moveTalkSelect = new OO.ui.FieldLayout(new OO.ui.CheckboxInputWidget({
            id: "bm-movetalk-box",
            selected: true,
        }), {
            label: "移动关联的讨论页",
            align: "inline",
            id: "bm-movetalk",
        });
        const redirectWidget = new OO.ui.CheckboxInputWidget({ id: "bm-redirect-box" });
        const noredirectSelect = new OO.ui.FieldLayout(redirectWidget, {
            label: "保留重定向",
            align: "inline",
            id: "bm-redirect",
        });
        const watchlistSelect = new OO.ui.FieldLayout(new OO.ui.CheckboxInputWidget({
            id: "bm-watchlist-box",
        }), {
            label: "监视源页面和目标页面",
            align: "inline",
            id: "bm-watchlist",
        });
        $("#bm-option").append(
            moveTalkSelect.$element,
            noredirectSelect.$element,
            watchlistSelect.$element,
        );

        // 操作面板
        const submitButton = new OO.ui.ButtonWidget({
            label: "提交",
            flags: [
                "primary",
                "progressive",
            ],
            icon: "check",
            id: "bm-submit",
        });
        const intervalBox = new OO.ui.TextInputWidget({
            placeholder: "操作间隔",
            id: "bm-interval",
        });
        const reasonBox = new OO.ui.TextInputWidget({
            placeholder: "附加摘要",
            id: "bm-reason",
        });
        $("#bm-submit-panel").append(
            submitButton.$element,
            intervalBox.$element,
            reasonBox.$element,
        );
        mw.user.getRights().done((result) => {
            if(!result.includes("suppressredirect")) {
                redirectWidget.setSelected(true).setDisabled(true);
            }
        });

        // 点击按钮添加行
        $("#bm-add-row").on("click", () => addRow());

        // 点击按钮删除行
        $("#bm-page-list-table").on("click", ".remove-row", function () {
            rowCount--;
            $(this).closest("tr").remove();
            $("#bm-page-list-table tbody tr").each((i, ele) => {
                $(ele).find("input").attr("data-row-no", i + 1);
            });
        });

        // 从剪贴板粘贴
        $("#bm-page-list-table").on("paste", 'input[type="text"]', function (e) {
            navigator.clipboard.readText().then((text) => {
                if (text.indexOf("\t") > -1 || text.indexOf("\n") > -1 && text.indexOf("\n") !== text.length - 1) {
                    e.preventDefault();
                    const rows = text.split("\n");
                    for (let i = 0; i < rows.length; i++) {
                        const columns = rows[i].split("\t");
                        for (let j = 0; j < 2; j++) {
                            if (columns[j]?.trim().length > 0) { // 经过split可能产生空字符串,要去掉
                                $(`#bm-page-list-table input[data-row-no="${i + Number($(this).attr("data-row-no"))}"][data-col-no="${j + Number($(this).attr("data-col-no"))}"]`).val(columns[j]);
                            }
                        }
                    }
                }
            });
        });

        // 清空日志
        $("#bm-log-clear").on("click", () => {
            $("#bm-log-lines").html("");
            $("#state-success, #state-nochange, #state-error").text(0);
            successCount = 0;
            errorCount = 0;
        });

        // 日志筛选
        $("#bm-log-state>div").each((_, ele) => {
            let show = true;
            const $ele = $(ele);
            $ele.on("click", (e) => {
                e.preventDefault();
                if (show) {
                    $("#bm-log-lines").addClass(`${$ele.attr("id")}-hide`);
                    $ele.removeClass("log-selected");
                    show = false;
                } else {
                    $("#bm-log-lines").removeClass(`${$ele.attr("id")}-hide`);
                    $ele.addClass("log-selected");
                    show = true;
                }
            });
        });

        // 执行体
        submitButton.on("click", async () => {
            const confirmText = $("<p>请确认您的移动是否有误。若因输入不当而产生错误,请自行<ruby><rb>承担后果</rb><rp>(</rp><rt>料理后事</rt><rp>)</rp></ruby>。</p>");
            const confirm = await OO.ui.confirm(confirmText, {
                title: "提醒",
                size: "small",
            });
            if (confirm) {
                submitButton.setDisabled(true);
                window.onbeforeunload = () => true;
                $("#mw-content-text input, #mw-content-text textarea").prop("disabled", true);

                const movetalk = $("#bm-movetalk-box input").prop("checked");
                const noredirect = !$("#bm-redirect-box input").prop("checked");
                const watchlist = $("#bm-watchlist-box input").prop("checked") ? "watch" : "unwatch";
                const reason = reasonBox.getValue().length > 0 ? `[[User:BearBin/js#批量移动页面|BulkMove]]:${reasonBox.getValue()}` : "[[User:BearBin/js#批量移动页面|BulkMove]]";
                const interval = Number(intervalBox.getValue());

                $("#bm-page-list-table tbody tr").each(async (_, tr) => {
                    const from = $(tr).find("input")[0].value;
                    const to = $(tr).find("input")[1].value;
                    if (from?.length > 0 && to?.length > 0) {
                        try {
                            const result = await api.postWithToken("csrf", {
                                format: "json",
                                action: "move",
                                from,
                                to,
                                movetalk,
                                noredirect,
                                watchlist,
                                reason,
                                tags: "Automation tool",
                                bot: true,
                            });
                            if (result.move) {
                                record(`移动【<a href="/${from}${noredirect ? "" : "?redirect=no"}" class="${noredirect ? "" : "mw-redirect"}">${from}</a>】→【<a href="/${to}">${to}</a>】成功。`, "success");
                                $("#state-success").text(++successCount);
                                await waitInterval(interval);
                            }
                        } catch (e) {
                            let errorMessage = "";
                            switch(e) {
                                case "missingtitle":
                                    errorMessage = "源页面不存在";
                                    break;
                                case "articleexists":
                                    errorMessage = "目标页面已存在";
                                    break;
                                case "http":
                                    errorMessage = "网络连接出错";
                                    break;
                                default:
                                    errorMessage = e;
                            }
                            record(`<a href="/${from}">${from}</a>→<a href="/${to}">${to}</a>移动失败:${errorMessage}。`, "error");
                            $("#state-error").text(++errorCount);
                        }
                    }
                });
                submitButton.setDisabled(false);
                window.onbeforeunload = () => null;
                $("#mw-content-text input, #mw-content-text textarea").prop("disabled", false);
            }
        });
    }
})());
// </pre>