首页
社区
新闻
视频博客
锦标赛
排行榜
接受挑战
维基
论坛
navigation
首页
最近更改
随机页面
MediaWiki帮助
TOOLBOX
链入页面
相关更改
特殊页面
页面信息
cn
RU
EN
DE
个人工具
登录
查看“︁MediaWiki:BatchDelete.js”︁的源代码
来自3D坦克-WIKI
←
MediaWiki:BatchDelete.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
/* Wiki 批量删除工具 v1.0 — 放入 MediaWiki:BatchDelete.js * 入口:Project:批量删除。使用当前登录账号及本站 API,不发送数据至外站。 * 只删除人工输入并经预检、二次确认的页面;不递归删除链接、子页面或文件实体。 */ (function () { 'use strict'; if (mw.config.get('wgNamespaceNumber') !== 4 || mw.config.get('wgTitle') !== '批量删除' || mw.config.get('wgAction') !== 'view') { return; } if (window.wikiBatchDeleteLoaded) { return; } window.wikiBatchDeleteLoaded = true; mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () { $(function () { var root = document.getElementById('wiki-batch-delete'); if (!root) { root = document.createElement('div'); (document.getElementById('mw-content-text') || document.body).appendChild(root); } var api = new mw.Api(), account = null, rightsOK = false; var busy = false, running = false, stopping = false, ready = false; var records = [], activeJob = null; var LIMIT = 500; mw.util.addCSS( '#wiki-batch-delete{font-size:14px;line-height:1.6;max-width:1100px;color:inherit}' + '#wiki-batch-delete *{box-sizing:border-box}' + '#wiki-batch-delete .bd-card{border:1px solid #a2a9b1;border-radius:8px;padding:20px;margin:14px 0}' + '#wiki-batch-delete .bd-warning{border-left:4px solid #d33;padding:12px 16px;background:#fff4f3;color:#202122}' + '#wiki-batch-delete label{display:block;font-weight:bold;margin:12px 0 6px}' + '#wiki-batch-delete textarea,#wiki-batch-delete input[type=text]{display:block;width:100%;padding:9px;border:1px solid #72777d;border-radius:4px;background:#fff;color:#202122;font:inherit}' + '#wiki-batch-delete textarea{min-height:210px;font-family:monospace}' + '#wiki-batch-delete button{padding:8px 14px;margin:6px 8px 6px 0;border:1px solid #72777d;border-radius:4px;cursor:pointer;background:#f8f9fa;color:#202122;font:inherit}' + '#wiki-batch-delete button.bd-primary{background:#36c;border-color:#36c;color:#fff}' + '#wiki-batch-delete button.bd-danger{background:#b32424;border-color:#b32424;color:#fff}' + '#wiki-batch-delete button:disabled{opacity:.45;cursor:not-allowed}' + '#wiki-batch-delete .bd-help{color:#54595d;font-size:13px}' + '#wiki-batch-delete .bd-scroll{overflow:auto;max-height:520px}' + '#wiki-batch-delete table{border-collapse:collapse;width:100%;font-size:13px}' + '#wiki-batch-delete th,#wiki-batch-delete td{border-bottom:1px solid #a2a9b1;padding:9px;text-align:left;vertical-align:top;overflow-wrap:anywhere}' + '#wiki-batch-delete th{background:#eaecf0;color:#202122;position:sticky;top:0}' + '#wiki-batch-delete progress{width:100%;height:18px}' + '#wiki-batch-delete .bd-check{font-weight:normal;display:flex;align-items:flex-start;gap:8px}' + '#wiki-batch-delete .bd-check input{margin-top:6px}' + '#wiki-batch-delete .bd-status{white-space:pre-wrap}' ); root.innerHTML = '<h2>批量删除页面</h2>' + '<div class="bd-warning"><b>此操作会删除页面及其修订历史。</b>通常可由具有恢复权限的管理员恢复,但请先备份。' + '本工具不清理缓存、关联图片、入链或子页面,也不会绕过站点权限。</div>' + '<p id="bd-permission" role="status">正在检查当前账号权限……</p>' + '<div class="bd-card">' + '<label for="bd-titles">页面名称 · 每行一个</label>' + '<textarea id="bd-titles" placeholder="示例页面一\n示例页面二\n模板:示例模板" spellcheck="false"></textarea>' + '<p class="bd-help">最多 500 个不重复名称。输入完整页面名称,不要输入网址、[[链接语法]]或 #章节。' + '空行和重复名称自动忽略;重定向只删除重定向页本身。文件及其讨论页、MediaWiki 界面页及其讨论页、本工具入口受到安全限制。</p>' + '<label for="bd-reason">删除理由(必填,会进入公开删除日志)</label>' + '<input id="bd-reason" type="text" maxlength="150" placeholder="例如:清理不适用于国服的外服赛事及平台内容">' + '<button id="bd-preview" class="bd-primary" disabled>① 检查并预览</button>' + '<button id="bd-export" disabled>下载 CSV 日志</button>' + '</div>' + '<div class="bd-card">' + '<p id="bd-summary" role="status">尚未检查页面。</p>' + '<progress id="bd-progress" max="1" value="0"></progress>' + '<div class="bd-scroll"><table><thead><tr><th>#</th><th>页面</th><th>状态</th><th>详情</th></tr></thead><tbody id="bd-rows"></tbody></table></div>' + '</div>' + '<div class="bd-card">' + '<label class="bd-check"><input id="bd-agree" type="checkbox">' + '<span>我已检查以上列表并做好备份,同意删除所有标记为“待删除”的页面及其历史修订。</span></label>' + '<label for="bd-confirm">输入“确认删除”</label><input id="bd-confirm" type="text" autocomplete="off">' + '<button id="bd-delete" class="bd-danger" disabled>② 执行批量删除</button>' + '<button id="bd-stop" disabled>停止后续删除</button>' + '<p class="bd-help">逐页串行执行,约每秒处理一页。停止不会撤销已完成的删除,也不能取消已经发送的请求。' + '预检后有新修订或页面 ID 变化的页面会跳过;发生结果不明或权限错误时自动停止。失败后请核实删除日志,再重新预检,不自动重试。</p>' + '<p id="bd-status" class="bd-status" role="status" aria-live="polite"></p>' + '</div>'; function el(id) { return root.querySelector('#bd-' + id); } function text(id, value) { el(id).textContent = value; } function pending() { return records.filter(function (r) { return r.status === '待删除'; }); } function status(message) { text('status', message); } function updateButtons() { el('preview').disabled = busy || !rightsOK; el('titles').disabled = busy; el('reason').disabled = busy; el('agree').disabled = busy || !ready; el('confirm').disabled = busy || !ready; el('delete').disabled = busy || !ready || !rightsOK || !pending().length || !el('reason').value.trim() || !el('agree').checked || el('confirm').value.trim() !== '确认删除'; el('stop').disabled = !running || stopping; el('export').disabled = !records.length; } function invalidate() { if (busy) { return; } ready = false; el('agree').checked = false; el('confirm').value = ''; status('输入已变更,请重新检查并预览。'); updateButtons(); } function request(params, write) { return new Promise(function (resolve, reject) { var q = write ? api.postWithToken('csrf', params) : api.post(params); q.then(resolve, function (code, result) { var detail = result && result.error && result.error.info; if (!detail && result && result.textStatus) { detail = result.textStatus; } reject({ code: String(code || 'unknown'), info: String(detail || code || '请求失败') }); }); }); } function identity() { return request({ action: 'query', meta: 'userinfo', uiprop: 'rights', formatversion: 2 }).then(function (data) { var u = data.query && data.query.userinfo; if (!u || !u.id || (u.rights || []).indexOf('delete') === -1) { throw { code: 'permissiondenied', info: '当前账号未登录或没有 delete(删除页面)权限。界面管理员身份不一定包含此权限。' }; } if (account && account.id !== u.id) { throw { code: 'accountchanged', info: '登录账号发生变化,请刷新本页面后重试。' }; } return u; }); } function render() { var body = el('rows'); body.textContent = ''; records.forEach(function (r, i) { var tr = document.createElement('tr'); [String(i + 1), r.title, r.status, r.detail || ''].forEach(function (s, j) { var td = document.createElement('td'); if (j === 1 && r.pageid) { var a = document.createElement('a'); a.href = mw.util.getUrl(r.title); a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = s; td.appendChild(a); } else { td.textContent = s; } tr.appendChild(td); }); body.appendChild(tr); }); var counts = {}; records.forEach(function (r) { counts[r.status] = (counts[r.status] || 0) + 1; }); text('summary', '共 ' + records.length + ' 页 · ' + Object.keys(counts).map(function (k) { return k + ' ' + counts[k]; }).join(' / ')); } function unsafePage(p) { if ([6, 7, 8, 9].indexOf(p.ns) !== -1) { return '安全限制:不批量删除文件、文件讨论、界面及界面讨论页面。'; } if (p.ns === 4 && p.title.slice(p.title.indexOf(':') + 1) === '批量删除') { return '安全限制:不能删除本工具入口。'; } return ''; } function collect() { var seen = Object.create(null), result = []; el('titles').value.split(/\r?\n/).forEach(function (line) { var s = line.trim().replace(/_/g, ' '); if (!s || seen[s]) { return; } seen[s] = true; var r = { input: s, title: s, status: '待检查', detail: '', pageid: null, revid: null, time: '', reason: '' }; if (/[\[\]#|<>\u0000-\u001f]/.test(s) || /^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(s)) { r.status = '输入无效'; r.detail = '请填写页面名称,不要填写网址、链接语法、章节或控制字符。'; } result.push(r); }); return result; } async function preview() { if (busy) { return; } var next = collect(); if (!next.length) { status('请至少输入一个页面名称。'); return; } if (next.length > LIMIT) { status('一次最多检查 500 页,请分批执行。'); return; } if (!el('reason').value.trim()) { status('请填写删除理由。'); return; } records = next; busy = true; ready = false; el('agree').checked = false; el('confirm').value = ''; el('progress').max = records.length; el('progress').value = 0; records.forEach(function (r) { r.reason = el('reason').value.trim(); }); updateButtons(); render(); status('正在预检,尚未执行任何删除……'); try { await identity(); var list = records.filter(function (r) { return r.status === '待检查'; }); var seenIds = Object.create(null); for (var i = 0; i < list.length; i += 50) { var batch = list.slice(i, i + 50); var data = await request({ action: 'query', prop: 'info', titles: batch.map(function (r) { return r.title; }).join('|'), assert: 'user', formatversion: 2 }); var q = data.query || {}, aliases = Object.create(null), pages = q.pages || []; (q.normalized || []).concat(q.converted || []).forEach(function (n) { aliases[n.from] = n.to; }); function canonical(title) { var visited = Object.create(null); while (aliases[title] && !visited[title]) { visited[title] = true; title = aliases[title]; } return title; } batch.forEach(function (r) { r.title = canonical(r.title); var p = pages.find(function (x) { return x.title === r.title; }); if (!p) { r.status = '跳过'; r.detail = '未找到本站页面信息(可能为跨站名称)。'; return; } if (p.invalid || p.ns < 0) { r.status = '输入无效'; r.detail = p.invalidreason || '无效或特殊页面。'; return; } if (p.missing || !p.pageid) { r.status = '不存在'; r.detail = '无需删除。'; return; } r.pageid = p.pageid; r.revid = p.lastrevid; r.title = p.title; var blocked = unsafePage(p); if (blocked) { r.status = '跳过'; r.detail = blocked; return; } if (seenIds[p.pageid]) { r.status = '重复页面'; r.detail = '规范化后与其他输入相同,不重复操作。'; return; } seenIds[p.pageid] = true; r.status = '待删除'; r.detail = '页面 ID ' + p.pageid + ';最新修订 ' + p.lastrevid + (p.redirect ? ';这是重定向页,只删除此页。' : ''); }); el('progress').value = Math.min(records.length, i + batch.length); render(); } ready = true; el('progress').value = records.length; status('预检完成。请核对列表,再勾选确认并输入“确认删除”。'); } catch (e) { ready = false; records.forEach(function (r) { if (r.status === '待检查') { r.status = '检查失败'; r.detail = e.info || String(e); } }); status('预检未完成,禁止执行删除:' + (e.info || String(e))); } finally { busy = false; render(); updateButtons(); } } function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } async function execute() { if (el('delete').disabled || busy) { return; } var jobs = pending().slice(), reason = el('reason').value.trim(); if (!window.confirm('最后确认:以“' + account.name + '”删除 ' + jobs.length + ' 个页面及其修订历史?\n\n理由:' + reason + '\n\n已完成的删除不会因点击停止而撤销。')) { return; } busy = true; running = true; stopping = false; ready = false; el('progress').max = jobs.length; el('progress').value = 0; updateButtons(); var processed = 0; try { await identity(); for (var i = 0; i < jobs.length; i++) { if (stopping) { break; } var r = jobs[i]; activeJob = r; r.status = '核对中'; render(); status('正在处理 ' + (i + 1) + ' / ' + jobs.length + ':' + r.title); var sentDelete = false; try { // Preflight by title: a move/recreation/revision change since preview is skipped. var check = await request({ action: 'query', titles: r.title, prop: 'info', formatversion: 2, assert: 'user' }); var p = check.query && check.query.pages && check.query.pages[0]; if (stopping) { r.status = '未执行'; r.detail = '已停止,未发送删除请求。'; break; } if (!p || p.missing || p.invalid || p.pageid !== r.pageid || p.lastrevid !== r.revid || p.title !== r.title) { r.status = '变化跳过'; r.detail = '预检后页面已改动、移动、重建或删除。请重新检查。'; } else { r.status = '删除中'; render(); sentDelete = true; var result = await request({ action: 'delete', pageid: r.pageid, reason: reason, assert: 'user', assertuser: account.name, watchlist: 'nochange', formatversion: 2, maxlag: 5 }, true); if (!result.delete || result.delete.pageid !== r.pageid) { throw { code: 'unexpected-response', info: '未收到可核实的删除成功响应,请检查本站删除日志。' }; } r.status = '已删除'; r.detail = '删除成功;' + (result.delete.title || r.title); if (result.delete.logid) { r.detail += ';日志 ID ' + result.delete.logid; } } } catch (e) { var code = e.code || 'unknown'; var uncertain = sentDelete && ['http', 'unknown', 'unexpected-response', 'timeout', 'abort'].indexOf(code) !== -1; r.status = uncertain ? '结果待核实' : '失败'; r.detail = code + ':' + (e.info || String(e)); // Do not retry deletion. Stop on any exception; user must verify current state first. stopping = true; status('已暂停:' + r.title + '\n' + r.detail + '\n请检查页面和本站删除日志,再重新预检。'); } finally { r.time = new Date().toISOString(); r.reason = reason; processed++; el('progress').value = processed; render(); } activeJob = null; if (!stopping && i < jobs.length - 1) { await sleep(1000); } } } catch (e) { stopping = true; status('无法继续:' + (e.info || String(e))); } finally { jobs.forEach(function (r) { if (r.status === '待删除' || r.status === '核对中') { r.status = '未执行'; r.detail = '批次停止,未发送删除请求。'; } }); activeJob = null; busy = false; running = false; ready = false; el('agree').checked = false; el('confirm').value = ''; render(); updateButtons(); if (!stopping) { status('本批次处理完成。建议下载日志留档。再次执行需要重新预检。'); } else { status(el('status').textContent + '\n批次已停止。已完成的删除未撤销;请下载日志。'); } } } function csvCell(value) { var s = String(value == null ? '' : value); // Protect spreadsheet formula injection, including page names beginning with =,+,-,@. if (/^[\s]*[=+\-@]/.test(s)) { s = "'" + s; } return '"' + s.replace(/"/g, '""') + '"'; } function exportLog() { var rows = [['序号', '输入名称', '规范页面名', '页面ID', '预检修订ID', '状态', '详情', '删除理由', '处理时间UTC', '操作账号']]; records.forEach(function (r, i) { rows.push([i + 1, r.input, r.title, r.pageid, r.revid, r.status, r.detail, r.reason, r.time, account ? account.name : '']); }); var csv = '\uFEFF' + rows.map(function (row) { return row.map(csvCell).join(','); }).join('\r\n'); var url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' })); var a = document.createElement('a'); a.href = url; a.download = 'Wiki批量删除日志-' + new Date().toISOString().replace(/[:.]/g, '-') + '.csv'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(function () { URL.revokeObjectURL(url); }, 30000); } el('titles').addEventListener('input', invalidate); el('reason').addEventListener('input', invalidate); el('agree').addEventListener('change', updateButtons); el('confirm').addEventListener('input', updateButtons); el('preview').addEventListener('click', preview); el('delete').addEventListener('click', execute); el('stop').addEventListener('click', function () { stopping = true; updateButtons(); status('已请求停止。' + (activeJob ? '等待当前步骤返回:' + activeJob.title : '不再开始下一页。')); }); el('export').addEventListener('click', exportLog); window.addEventListener('beforeunload', function (event) { if (busy) { event.preventDefault(); event.returnValue = ''; } }); identity().then(function (u) { account = { id: u.id, name: u.name }; rightsOK = true; text('permission', '当前账号:' + u.name + ' · 已具备删除页面权限。'); updateButtons(); }).catch(function (e) { rightsOK = false; text('permission', e.info || String(e)); updateButtons(); }); updateButtons(); }); }).catch(function () { var root = document.getElementById('wiki-batch-delete'); if (root) { root.textContent = '批量删除工具加载失败,请刷新页面或检查浏览器控制台。'; } }); }());
返回
MediaWiki:BatchDelete.js
。
反馈问题
导航菜单
导航
首页
最近更改
随机页面
MediaWiki帮助
提交支持工单
您的支持请求已受理。技术支持团队会尽快与您联系。
搜索
报告 Wiki 中的错误
请
不要
使用此表单申诉封号或封锁处罚。此表单仅用于反馈
与 Wiki 相关
的错误或改进建议。
您的游戏昵称
页面链接
我们已为您自动填写此项,如有误,请从浏览器地址栏复制所需页面的链接并粘贴至此处。
留言内容
请告诉我们您希望修改该页面的哪些内容,或指出其中的错误/不准确之处。可附带截图链接。