用户:Hehua/deletebycategory.js
来自Postal Wiki
< 用户:Hehua
注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
/*!
* MediaWiki 小工具:按分类批量删除页面
* ---------------------------------------------------------------------------
* 用途:列出某个分类(默认 Category:快速删除候选)内的页面,在原生 OOUI 界面中
* 勾选后批量删除;可选同时删除对应讨论页、以及指向这些页面的重定向。
*
* 安装方式(任选其一):
* 1) 粘贴到 [[MediaWiki:Common.js]](需 interface-admin / sysop 权限);
* 2) 粘贴到你的 [[Special:MyPage/common.js]],仅自己可用;
* 3) 放在 [[MediaWiki:Gadget-xxx.js]] 并在 [[MediaWiki:Gadgets-definition]] 中启用;
* 4) 其他站点用 mw.loader.load( 'https://你的站点/...js' ) 或 importScript 引。
*
* 权限要求:当前用户必须拥有 delete 权限(管理员),否则删除会返回 permissiondenied。
*
* 使用:任意页面左侧工具栏(工具箱)“批量删除分类页面”,点击后弹出对话框。
* 也可在控制台执行 mw.libs.batchDeleteByCategory.open()。
*
* 可用配置(放在脚本加载之前即可,全部可选):
* window.BatchDeleteByCategoryConfig = {
* defaultCategory: 'Category:快速删除候选', // 默认分类
* defaultReason: '', // 默认删除理由
* portletLabel: '批量删除分类页面', // 侧栏链接文字
* portletId: 't-batch-delete-category',
* pageLimit: 2000, // 单次最多载入页面数
* delay: 250, // 每次 API 请求间隔(ms),防限流
* watchlist: 'nochange', // nochange / preferences / watch
* talkReasonSuffix: '(删除讨论页)',
* redirectReasonSuffix: '(清理重定向)',
* addPortletLink: true, // 是否自动添加侧栏链接
* checkRights: true // 打开时检查是否有 delete 权限
* };
*
* 依赖:mediawiki.api、oojs-ui(1.35+ 均自带)。
* 许可证:CC0 / 公有领域,随意修改。
*/
( function () {
'use strict';
if ( typeof mw === 'undefined' || !mw.loader || !window.jQuery || !mw.Api ) {
return;
}
var $ = window.jQuery;
var api = new mw.Api();
/* ========================================================================
* 配置
* ===================================================================== */
var conf = $.extend( {
defaultCategory: 'Category:快速删除候选',
defaultReason: '',
portletLabel: '批量删除分类页面',
portletId: 't-batch-delete-category',
pageLimit: 2000,
delay: 250,
watchlist: 'nochange',
talkReasonSuffix: '(删除讨论页)',
redirectReasonSuffix: '(清理重定向)',
addPortletLink: true,
checkRights: true
}, window.BatchDeleteByCategoryConfig || {} );
var STATUS_TEXT = {
ok: '成功',
missing: '跳过',
skip: '跳过',
fail: '失败',
dry: '试运行'
};
// 这些错误说明令牌/权限/数据库整体不可用,继续跑下去没有意义,直接中止整批
var FATAL_CODES = {
permissiondenied: true,
badtoken: true,
readonly: true,
blocked: true,
autoblocked: true,
mustbeloggedin: true,
'internal_api_error_DBQueryError': true
};
/* ========================================================================
* 通用工具
* ===================================================================== */
function sleep( ms ) {
var d = $.Deferred();
window.setTimeout( function () {
d.resolve();
}, ms );
return d.promise();
}
// 串行执行,保证请求有间隔、日志顺序正确
function eachSeq( list, fn ) {
var d = $.Deferred();
var i = 0;
function next() {
if ( i >= list.length ) {
d.resolve();
return;
}
var item = list[ i ];
i++;
var result;
try {
result = fn( item );
} catch ( e ) {
d.reject( e );
return;
}
$.when( result ).then( next, function ( e ) {
d.reject( e );
} );
}
next();
return d.promise();
}
function categoryNamespace() {
var ns = mw.config.get( 'wgFormattedNamespaces' ) || {};
return ns[ 14 ] || ns[ '14' ] || 'Category';
}
// 允许只填“快速删除候选”,自动补上分类命名空间
function normalizeCategory( value ) {
var text = $.trim( value || '' ).replace( /_/g, ' ' );
if ( !text ) {
return '';
}
if ( text.indexOf( ':' ) === -1 ) {
text = categoryNamespace() + ':' + text;
}
return text;
}
// 取讨论页标题;没有对应讨论页(如讨论页自身/特殊页)时返回 null
function talkTitleOf( title ) {
var t = mw.Title.newFromText( title );
if ( !t ) {
return null;
}
var talk = t.getTalkPage();
if ( !talk ) {
return null;
}
var talkText = talk.getPrefixedText();
return talkText === t.getPrefixedText() ? null : talkText;
}
/* ========================================================================
* API 封装
* ===================================================================== */
// 读取分类成员;返回 { pages: [...], subcats: [...] }
function fetchCategoryMembers( cat ) {
var result = { pages: [], subcats: [] };
function step( cont ) {
var params = {
action: 'query',
formatversion: 2,
generator: 'categorymembers',
gcmtitle: cat,
gcmtype: 'page|subcat|file',
gcmlimit: 'max',
prop: 'info'
};
if ( cont ) {
$.extend( params, cont );
}
return api.get( params ).then( function ( data ) {
var pages = ( data.query && data.query.pages ) || [];
pages.forEach( function ( p ) {
if ( p.ns === 14 ) {
result.subcats.push( p.title );
} else if ( !p.missing ) {
result.pages.push( {
title: p.title,
ns: p.ns,
redirect: !!p.redirect,
length: p.length || 0
} );
}
} );
if ( data.continue ) {
return step( data.continue );
}
return result;
} );
}
return step();
}
// 递归收集分类内页面(BFS,带深度限制)
function collectPages( rootCat, opts, onStatus ) {
var d = $.Deferred();
var pages = [];
var seen = {};
var visitedCats = {};
var allSubcats = {};
var queue = [ { cat: rootCat, depth: 0 } ];
function addPages( list ) {
list.forEach( function ( p ) {
if ( pages.length >= opts.limit || seen[ p.title ] ) {
return;
}
seen[ p.title ] = true;
pages.push( p );
} );
}
function next() {
if ( !queue.length || pages.length >= opts.limit ) {
d.resolve( { pages: pages, subcats: Object.keys( allSubcats ) } );
return;
}
var item = queue.shift();
var key = item.cat.toLowerCase();
if ( visitedCats[ key ] ) {
return next();
}
visitedCats[ key ] = true;
onStatus( '正在读取 ' + item.cat + ' …' );
fetchCategoryMembers( item.cat ).then( function ( res ) {
addPages( res.pages );
res.subcats.forEach( function ( sc ) {
allSubcats[ sc ] = true;
if ( opts.recursive && item.depth < opts.depth ) {
queue.push( { cat: sc, depth: item.depth + 1 } );
}
} );
next();
}, function ( code, data ) {
// 分类不存在/标题非法时忽略,继续处理队列中的其他分类
if ( code === 'invalidtitle' || code === 'invalidcategory' ||
( data && data.error && /invalid/i.test( data.error.code || '' ) ) ) {
next();
return;
}
d.reject( code, data );
} );
}
next();
return d.promise();
}
// 列出指向该标题的所有重定向
function getRedirectsTo( title ) {
var result = [];
function step( cont ) {
var params = {
action: 'query',
formatversion: 2,
titles: title,
prop: 'redirects',
rdprop: 'title|fragment',
rdlimit: 'max'
};
if ( cont ) {
$.extend( params, cont );
}
return api.get( params ).then( function ( data ) {
var page = data.query && data.query.pages && data.query.pages[ 0 ];
if ( page && page.redirects ) {
page.redirects.forEach( function ( r ) {
result.push( r.title );
} );
}
if ( data.continue ) {
return step( data.continue );
}
return result;
} );
}
return step();
}
// 删除单个页面;返回 { title, status, code, info },不向外抛错
function deletePage( title, reason, dryRun, watchlist ) {
if ( dryRun ) {
return $.Deferred().resolve( { title: title, status: 'dry' } ).promise();
}
return api.postWithToken( 'csrf', {
action: 'delete',
title: title,
reason: reason || '',
watchlist: watchlist || 'nochange'
} ).then( function () {
return { title: title, status: 'ok' };
}, function ( code, data ) {
if ( code === 'missingtitle' ) {
return { title: title, status: 'missing', info: '页面不存在' };
}
var info = ( data && data.error && data.error.info ) || code;
return { title: title, status: 'fail', code: code, info: info };
} );
}
/* ========================================================================
* 删除流程(单页 / 批量)
* ===================================================================== */
function processOne( item, o, report ) {
var redirects = [];
var mainStatus = null;
var chain = $.Deferred().resolve().promise();
// 1. 先把重定向列表取出来(页面删掉后就查不到了)
if ( o.deleteRedirects ) {
chain = chain.then( function () {
return getRedirectsTo( item.title ).then( function ( list ) {
redirects = list;
}, function () {
redirects = [];
} );
} );
}
// 2. 删除主页面
chain = chain.then( function () {
return deletePage( item.title, o.reason, o.dryRun, o.watchlist ).then( function ( r ) {
mainStatus = r.status;
report( { title: r.title, status: r.status, code: r.code, info: r.info, kind: '页面' } );
} );
} );
// 3. 删除对应讨论页
if ( o.deleteTalk ) {
chain = chain.then( function () {
var talk = talkTitleOf( item.title );
if ( !talk ) {
return null;
}
if ( mainStatus === 'fail' ) {
report( { title: talk, status: 'skip', info: '主页面删除失败,已跳过讨论页', kind: '讨论页' } );
return null;
}
return deletePage( talk, o.talkReason, o.dryRun, o.watchlist ).then( function ( r ) {
report( { title: r.title, status: r.status, code: r.code, info: r.info, kind: '讨论页' } );
} );
} );
}
// 4. 删除指向该页面的重定向
if ( o.deleteRedirects ) {
chain = chain.then( function () {
if ( !redirects.length || mainStatus === 'fail' ) {
return null;
}
return eachSeq( redirects, function ( rd ) {
if ( rd === item.title ) {
return null;
}
return deletePage( rd, o.redirectReason, o.dryRun, o.watchlist ).then( function ( r ) {
report( { title: r.title, status: r.status, code: r.code, info: r.info, kind: '重定向' } );
} );
} );
} );
}
return chain;
}
function runBatch( items, o, hooks ) {
var d = $.Deferred();
var i = 0;
var fatal = false;
function step() {
if ( fatal || hooks.shouldAbort() ) {
d.resolve();
return;
}
if ( i >= items.length ) {
d.resolve();
return;
}
var item = items[ i ];
i++;
hooks.onItem( i, items.length, item.title );
processOne( item, o, function ( entry ) {
if ( entry.code && FATAL_CODES[ entry.code ] ) {
fatal = true;
}
hooks.onReport( entry );
} ).then( function () {
if ( fatal ) {
hooks.onFatal();
d.resolve();
return;
}
sleep( o.delay ).then( step );
}, function ( e ) {
d.reject( e );
} );
}
step();
return d.promise();
}
/* ========================================================================
* 样式
* ===================================================================== */
function injectStyles() {
if ( document.getElementById( 'bdbc-styles' ) ) {
return;
}
var css = [
'.bdbc-list{border:1px solid var(--border-color-subtle,#c8ccd1);border-radius:2px;',
'max-height:38vh;overflow-y:auto;padding:4px 8px;margin:6px 0;',
'background:var(--background-color-base,#fff);color:var(--color-base,#202122);}',
'.bdbc-row{display:flex;align-items:center;gap:6px;padding:1px 0;line-height:1.75;font-size:.92em;}',
'.bdbc-row:hover{background:var(--background-color-interactive-subtle,#eaf3ff);}',
'.bdbc-row input{margin:0 2px 0 0;vertical-align:middle;}',
'.bdbc-row .bdbc-title{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
'.bdbc-row .bdbc-tag{color:var(--color-subtle,#72777d);font-size:.86em;white-space:nowrap;}',
'.bdbc-row-done{opacity:.55;text-decoration:line-through;}',
'.bdbc-log{display:none;max-height:16vh;overflow-y:auto;border:1px solid var(--border-color-subtle,#c8ccd1);',
'border-radius:2px;padding:6px 8px;margin-top:8px;font-family:monospace,monospace;font-size:.8em;',
'line-height:1.6;background:var(--background-color-neutral-subtle,#f8f9fa);color:var(--color-base,#202122);}',
'.bdbc-log .bdbc-ok{color:var(--color-success,#14866d);}',
'.bdbc-log .bdbc-fail{color:var(--color-error,#d73333);}',
'.bdbc-log .bdbc-skip{color:var(--color-subtle,#72777d);}',
'.bdbc-selection{margin-left:8px;color:var(--color-subtle,#54595d);}',
'.bdbc-muted{color:var(--color-subtle,#72777d);font-size:.85em;margin-top:4px;}',
'.bdbc-inline{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-top:4px;}',
'.bdbc-inline > .oo-ui-widget{flex:0 0 auto;margin:0;}',
'.bdbc-inline .oo-ui-dropdownInputWidget{width:7.5em;}',
'.bdbc-section{margin-top:12px;font-weight:bold;}'
].join( '' );
var style = document.createElement( 'style' );
style.id = 'bdbc-styles';
style.textContent = css;
document.head.appendChild( style );
}
/* ========================================================================
* 确认对话框(MessageDialog 子类,点“确认删除”后执行回调)
* ===================================================================== */
/* ========================================================================
* 主对话框
* ===================================================================== */
function Dialog( config ) {
Dialog.super.call( this, config );
}
OO.inheritClass( Dialog, OO.ui.ProcessDialog );
Dialog.static.name = 'batchDeleteByCategory';
Dialog.static.title = '批量删除分类内页面';
Dialog.static.actions = [
{ action: 'close', label: '关闭', flags: 'safe' },
{ action: 'abort', label: '停止', disabled: true },
{ action: 'delete', label: '删除选中页面', flags: [ 'primary', 'destructive' ], disabled: true }
];
Dialog.prototype.initialize = function () {
var dialog = this;
Dialog.super.prototype.initialize.call( this );
this.pages = [];
this.running = false;
this.aborted = false;
this.confirming = false;
/* ---------- 分类输入 ---------- */
this.categoryInput = new OO.ui.TextInputWidget( {
value: conf.defaultCategory,
placeholder: 'Category:快速删除候选'
} );
this.categoryInput.$element.on( 'keydown', function ( e ) {
if ( e.which === 13 ) {
dialog.loadPages();
}
} );
this.loadButton = new OO.ui.ButtonWidget( {
label: '载入页面',
flags: [ 'primary', 'progressive' ]
} );
this.loadButton.on( 'click', function () {
dialog.loadPages();
} );
this.recursiveCheck = new OO.ui.CheckboxInputWidget( { selected: false } );
this.depthSelect = new OO.ui.DropdownInputWidget( {
options: [
{ data: '1', label: '1 层' },
{ data: '2', label: '2 层' },
{ data: '3', label: '3 层' },
{ data: '5', label: '5 层' }
],
value: '1',
disabled: true
} );
this.recursiveCheck.on( 'change', function ( selected ) {
dialog.depthSelect.setDisabled( !selected );
} );
this.categoryField = new OO.ui.FieldLayout( this.categoryInput, {
label: '分类名称',
align: 'top',
help: '可省略「分类:」前缀。'
} );
this.recursiveField = new OO.ui.FieldLayout( this.recursiveCheck, {
label: '包含子分类',
align: 'inline'
} );
this.depthLabel = new OO.ui.LabelWidget( { label: '深度' } );
this.loadRow = new OO.ui.HorizontalLayout( {
items: [ this.loadButton, this.recursiveField, this.depthLabel, this.depthSelect ]
} );
this.loadRow.$element.addClass( 'bdbc-inline' );
/* ---------- 删除选项 ---------- */
this.talkCheck = new OO.ui.CheckboxInputWidget( { selected: false } );
this.redirectCheck = new OO.ui.CheckboxInputWidget( { selected: false } );
this.dryRunCheck = new OO.ui.CheckboxInputWidget( { selected: false } );
this.reasonInput = new OO.ui.TextInputWidget( {
value: conf.defaultReason,
placeholder: '删除理由(可留空)'
} );
this.optionsRow = new OO.ui.HorizontalLayout( {
items: [
new OO.ui.FieldLayout( this.talkCheck, {
label: '同时删除讨论页',
align: 'inline'
} ),
new OO.ui.FieldLayout( this.redirectCheck, {
label: '同时删除指向它们的重定向',
align: 'inline'
} ),
new OO.ui.FieldLayout( this.dryRunCheck, {
label: '试运行(不实际删除)',
align: 'inline'
} )
]
} );
this.optionsRow.$element.addClass( 'bdbc-inline' );
this.optionsFieldset = new OO.ui.FieldsetLayout( { label: '删除选项' } );
this.optionsFieldset.addItems( [
this.optionsRow,
new OO.ui.FieldLayout( this.reasonInput, { label: '删除理由', align: 'top' } )
] );
/* ---------- 页面列表 ---------- */
this.filterInput = new OO.ui.SearchInputWidget( {
placeholder: '筛选标题…',
disabled: true
} );
this.filterInput.on( 'change', function () {
dialog.applyFilter();
} );
this.selectAllButton = new OO.ui.ButtonWidget( { label: '全选', framed: false } );
this.selectNoneButton = new OO.ui.ButtonWidget( { label: '全不选', framed: false } );
this.invertButton = new OO.ui.ButtonWidget( { label: '反选', framed: false } );
this.selectAllButton.on( 'click', function () {
dialog.setVisible( true );
} );
this.selectNoneButton.on( 'click', function () {
dialog.setVisible( false );
} );
this.invertButton.on( 'click', function () {
dialog.invertVisible();
} );
this.selectionLabel = new OO.ui.LabelWidget( {
label: '尚未载入页面',
classes: [ 'bdbc-selection' ]
} );
this.selectRow = new OO.ui.HorizontalLayout( {
items: [
this.selectAllButton,
this.selectNoneButton,
this.invertButton,
this.selectionLabel
]
} );
this.selectRow.$element.addClass( 'bdbc-inline' );
this.listBox = $( '<div>' ).addClass( 'bdbc-list' ).text( '尚未载入页面。' );
/* ---------- 进度与日志 ---------- */
this.progressBar = new OO.ui.ProgressBarWidget( { progress: 0 } );
this.progressBar.$element.hide();
this.progressLabel = new OO.ui.LabelWidget( {
label: '就绪',
classes: [ 'bdbc-muted' ]
} );
this.logBox = $( '<div>' ).addClass( 'bdbc-log' );
/* ---------- 组装 ---------- */
this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
this.$body.append( this.content.$element );
this.content.$element.append(
this.categoryField.$element,
this.loadRow.$element,
this.optionsFieldset.$element,
$( '<div>' ).addClass( 'bdbc-section' ).text( '页面列表' ),
this.filterInput.$element,
this.selectRow.$element,
this.listBox,
this.progressBar.$element,
this.progressLabel.$element,
this.logBox
);
};
// 批量运行中关闭窗口时,先让当前页处理完再停
Dialog.prototype.getActionProcess = function ( action ) {
var dialog = this;
if ( action === 'close' ) {
return new OO.ui.Process( function () {
if ( dialog.running ) {
dialog.aborted = true;
}
dialog.close( { action: 'close' } );
}, this );
}
if ( action === 'abort' ) {
return new OO.ui.Process( function () {
dialog.abort();
}, this );
}
if ( action === 'delete' ) {
return new OO.ui.Process( function () {
dialog.confirmDelete();
}, this );
}
return Dialog.super.prototype.getActionProcess.call( this, action );
};
// 动作按钮在 setup 阶段才创建,ready 之后再同步可用状态
Dialog.prototype.getReadyProcess = function ( data ) {
var dialog = this;
return Dialog.super.prototype.getReadyProcess.call( this, data ).next( function () {
dialog.syncActions();
} );
};
// 内容变化后重新计算窗口尺寸,避免出现不必要的内部滚动
Dialog.prototype.refreshSize = function () {
var dialog = this;
window.setTimeout( function () {
try {
dialog.updateSize();
} catch ( e ) {
// 窗口已关闭时忽略
}
}, 10 );
};
/* ---------- 载入 ---------- */
Dialog.prototype.syncActions = function () {
if ( !this.actions.get().length ) {
return;
}
var selected = this.listBox ? this.listBox.find( 'input.bdbc-cb:checked' ).length : 0;
this.actions.setAbilities( {
delete: !this.running && !this.confirming && selected > 0,
abort: !!this.running
} );
};
Dialog.prototype.setBusy = function ( busy ) {
this.running = busy;
this.loadButton.setDisabled( busy );
this.syncActions();
};
Dialog.prototype.loadPages = function () {
var dialog = this;
if ( this.running ) {
return;
}
var cat = normalizeCategory( this.categoryInput.getValue() );
if ( !cat ) {
mw.notification.warn( '请先填写分类名称。' );
return;
}
var opts = {
recursive: this.recursiveCheck.isSelected(),
depth: parseInt( this.depthSelect.getValue(), 10 ) || 1,
limit: conf.pageLimit
};
this.pages = [];
this.listBox.text( '载入中…' );
this.progressLabel.setLabel( '正在读取分类成员…' );
this.progressBar.$element.show();
this.progressBar.setProgress( false );
this.setBusy( true );
collectPages( cat, opts, function ( status ) {
dialog.progressLabel.setLabel( status );
} ).then( function ( res ) {
dialog.renderPages( res.pages );
var msg = res.pages.length ?
'已载入 ' + res.pages.length + ' 个页面' :
'该分类下没有可删除的页面';
if ( res.subcats.length && !opts.recursive ) {
msg += '(另有 ' + res.subcats.length + ' 个子分类未包含,可勾选“包含子分类”)';
}
if ( res.pages.length >= opts.limit ) {
msg += '(已达上限 ' + opts.limit + ' 条)';
}
dialog.progressLabel.setLabel( msg );
dialog.filterInput.setDisabled( !res.pages.length );
}, function ( code, data ) {
var info = ( data && data.error && data.error.info ) || code || '未知错误';
dialog.listBox.text( '载入失败:' + info );
dialog.progressLabel.setLabel( '载入失败' );
mw.notification.error( '载入分类成员失败:' + info );
} ).always( function () {
dialog.progressBar.$element.hide();
dialog.setBusy( false );
dialog.refreshSize();
} );
};
Dialog.prototype.renderPages = function ( pages ) {
var dialog = this;
this.pages = pages || [];
this.listBox.empty();
this.filterInput.setValue( '' );
if ( !this.pages.length ) {
this.listBox.text( '该分类下没有可删除的页面。' );
this.updateSelection();
this.refreshSize();
return;
}
var frag = document.createDocumentFragment();
this.pages.forEach( function ( p, i ) {
var $row = $( '<div>' )
.addClass( 'bdbc-row' )
.attr( { 'data-index': i, 'data-title': p.title } );
var $cb = $( '<input>' )
.attr( { type: 'checkbox', 'data-index': i } )
.addClass( 'bdbc-cb' )
.prop( 'checked', true );
$cb.on( 'change', function () {
dialog.updateSelection();
} );
var $link = $( '<a>' )
.addClass( 'bdbc-title' )
.attr( {
href: mw.util.getUrl( p.title ),
target: '_blank',
rel: 'noopener',
title: p.title
} )
.text( p.title );
$row.append( $cb, $link );
var tags = [];
if ( p.redirect ) {
tags.push( '重定向' );
}
if ( p.ns === 6 ) {
tags.push( '文件' );
}
if ( p.length ) {
tags.push( p.length + ' 字节' );
}
if ( tags.length ) {
$row.append( $( '<span>' ).addClass( 'bdbc-tag' ).text( tags.join( ' · ' ) ) );
}
frag.appendChild( $row[ 0 ] );
} );
this.listBox.append( frag );
this.updateSelection();
this.refreshSize();
};
/* ---------- 选择 ---------- */
Dialog.prototype.updateSelection = function () {
var total = this.pages.length;
var selected = this.listBox.find( 'input.bdbc-cb:checked' ).length;
this.selectionLabel.setLabel( total ?
'已选 ' + selected + ' / ' + total + ' 个页面' :
'尚未载入页面' );
this.syncActions();
};
Dialog.prototype.getSelected = function () {
var dialog = this;
var out = [];
this.listBox.find( 'input.bdbc-cb:checked' ).each( function () {
var idx = parseInt( this.getAttribute( 'data-index' ), 10 );
if ( !isNaN( idx ) && dialog.pages[ idx ] ) {
out.push( dialog.pages[ idx ] );
}
} );
return out;
};
Dialog.prototype.visibleRows = function () {
return this.listBox.find( '.bdbc-row' ).filter( function () {
return $( this ).css( 'display' ) !== 'none';
} );
};
Dialog.prototype.setVisible = function ( checked ) {
this.visibleRows().find( 'input.bdbc-cb' ).each( function () {
if ( !this.disabled ) {
this.checked = checked;
}
} );
this.updateSelection();
};
Dialog.prototype.invertVisible = function () {
this.visibleRows().find( 'input.bdbc-cb' ).each( function () {
if ( !this.disabled ) {
this.checked = !this.checked;
}
} );
this.updateSelection();
};
Dialog.prototype.applyFilter = function () {
var q = $.trim( this.filterInput.getValue() ).toLowerCase();
this.listBox.find( '.bdbc-row' ).each( function () {
var title = ( $( this ).attr( 'data-title' ) || '' ).toLowerCase();
$( this ).toggle( !q || title.indexOf( q ) !== -1 );
} );
};
Dialog.prototype.markDone = function ( title ) {
this.listBox.find( '.bdbc-row' ).filter( function () {
return $( this ).attr( 'data-title' ) === title;
} ).addClass( 'bdbc-row-done' ).find( 'input.bdbc-cb' ).prop( {
checked: false,
disabled: true
} );
this.updateSelection();
};
/* ---------- 删除 ---------- */
Dialog.prototype.buildOptions = function () {
var base = $.trim( this.reasonInput.getValue() ) || conf.defaultReason;
return {
reason: base,
talkReason: base ? base + conf.talkReasonSuffix : '删除讨论页',
redirectReason: base ? base + conf.redirectReasonSuffix : '删除重定向',
deleteTalk: this.talkCheck.isSelected(),
deleteRedirects: this.redirectCheck.isSelected(),
dryRun: this.dryRunCheck.isSelected(),
watchlist: conf.watchlist
};
};
Dialog.prototype.confirmDelete = function () {
var dialog = this;
if ( this.confirming || this.running ) {
return;
}
var items = this.getSelected();
if ( !items.length ) {
return;
}
var o = this.buildOptions();
var $msg = $( '<div>' );
[
'分类:' + normalizeCategory( this.categoryInput.getValue() ),
'待删除页面:' + items.length + ' 个',
'删除理由:' + ( o.reason || '(留空)' ),
'同时删除讨论页:' + ( o.deleteTalk ? '是' : '否' ),
'同时删除重定向:' + ( o.deleteRedirects ? '是' : '否' )
].forEach( function ( line ) {
$msg.append( $( '<p>' ).text( line ) );
} );
if ( o.dryRun ) {
$msg.append( $( '<p>' ).text( '当前为试运行,只会输出将要执行的操作,不会真的删除。' ) );
} else {
$msg.append( $( '<p>' ).text( '删除后无法通过普通操作恢复,请确认。' ) );
}
// 用 OOUI 自带的全局 WindowManager(与 OO.ui.confirm 相同的做法),
// 避免同一个 WindowManager 里已有窗口打开时无法再开确认框
var lifecycle = OO.ui.getWindowManager().openWindow( 'message', {
title: o.dryRun ? '确认试运行' : '确认批量删除',
message: $msg,
actions: [
{
action: 'accept',
label: o.dryRun ? '开始试运行' : '确认删除',
flags: o.dryRun ? [ 'primary', 'progressive' ] : [ 'primary', 'destructive' ]
},
{ action: 'reject', label: '取消', flags: 'safe' }
]
} );
this.confirming = true;
this.syncActions();
// lifecycle.closed 是新版写法;旧版 openWindow 直接返回可 then 的 promise
var closed = lifecycle.closed || lifecycle;
closed.then( function ( data ) {
dialog.confirming = false;
dialog.syncActions();
if ( data && data.action === 'accept' ) {
dialog.executeDelete( items, o );
}
}, function () {
dialog.confirming = false;
dialog.syncActions();
mw.notification.error( '无法打开确认对话框,请稍后重试。' );
} );
};
Dialog.prototype.logLine = function ( text, type ) {
var $line = $( '<div>' ).text( text );
if ( type === 'ok' ) {
$line.addClass( 'bdbc-ok' );
} else if ( type === 'fail' ) {
$line.addClass( 'bdbc-fail' );
} else {
$line.addClass( 'bdbc-skip' );
}
this.logBox.append( $line );
this.logBox.scrollTop( this.logBox[ 0 ].scrollHeight );
};
Dialog.prototype.formatEntry = function ( entry ) {
var label = STATUS_TEXT[ entry.status ] || entry.status;
var line = '[' + label + '] ' + ( entry.title || '(无标题)' );
if ( entry.kind && entry.kind !== '页面' ) {
line += '(' + entry.kind + ')';
}
if ( entry.info ) {
line += ' — ' + entry.info;
}
return line;
};
Dialog.prototype.abort = function () {
if ( !this.running ) {
return;
}
this.aborted = true;
this.actions.setAbilities( { abort: false } );
this.progressLabel.setLabel( '正在停止(当前页面处理完后结束)…' );
};
Dialog.prototype.executeDelete = function ( items, o ) {
var dialog = this;
if ( this.running ) {
return;
}
var stats = { ok: 0, fail: 0, skip: 0, dry: 0 };
this.aborted = false;
this.logBox.show().empty();
this.progressBar.$element.show();
this.progressBar.setProgress( 0 );
this.setBusy( true );
this.logLine( '开始处理 ' + items.length + ' 个页面' + ( o.dryRun ? '(试运行)' : '' ), 'skip' );
this.refreshSize();
runBatch( items, {
reason: o.reason,
talkReason: o.talkReason,
redirectReason: o.redirectReason,
deleteTalk: o.deleteTalk,
deleteRedirects: o.deleteRedirects,
dryRun: o.dryRun,
watchlist: o.watchlist,
delay: conf.delay
}, {
shouldAbort: function () {
return dialog.aborted;
},
onItem: function ( i, n, title ) {
dialog.progressLabel.setLabel( '正在处理(' + i + '/' + n + '):' + title );
dialog.progressBar.setProgress( Math.round( ( i - 1 ) / n * 100 ) );
},
onReport: function ( entry ) {
dialog.logLine( dialog.formatEntry( entry ), entry.status );
if ( entry.status === 'ok' ) {
stats.ok++;
if ( entry.kind === '页面' ) {
dialog.markDone( entry.title );
}
} else if ( entry.status === 'dry' ) {
stats.dry++;
} else if ( entry.status === 'fail' ) {
stats.fail++;
} else {
stats.skip++;
}
},
onFatal: function () {
dialog.logLine( '检测到权限/令牌错误,已中止剩余页面的处理。', 'fail' );
}
} ).then( function () {
dialog.progressBar.setProgress( 100 );
dialog.progressLabel.setLabel( dialog.aborted ? '已停止' : '已完成' );
var summary = '结束:成功 ' + stats.ok + ' 项,跳过 ' + stats.skip + ' 项,失败 ' + stats.fail + ' 项' +
( o.dryRun ? ',试运行 ' + stats.dry + ' 项' : '' );
dialog.logLine( summary, stats.fail ? 'fail' : 'ok' );
dialog.setBusy( false );
dialog.refreshSize();
if ( !o.dryRun && stats.ok ) {
mw.notification.success( '批量删除完成:成功删除 ' + stats.ok + ' 项。' );
} else if ( !o.dryRun && stats.fail ) {
mw.notification.error( '批量删除结束,有 ' + stats.fail + ' 项失败,详见日志。' );
}
}, function () {
dialog.setBusy( false );
dialog.progressLabel.setLabel( '发生未预期的错误' );
dialog.logLine( '发生未预期的错误,已中止。', 'fail' );
} );
};
/* ========================================================================
* 启动
* ===================================================================== */
var windowManager = null;
var currentDialog = null;
function getWindowManager() {
if ( !windowManager ) {
windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );
}
return windowManager;
}
function openDialog() {
mw.loader.using( [
'oojs-ui',
'mediawiki.api',
'mediawiki.Title',
'mediawiki.util',
'mediawiki.notification'
] ).then( function () {
injectStyles();
var wm = getWindowManager();
// 同一个 WindowManager 同时只能开一个窗口,已经开着就直接复用
if ( currentDialog ) {
try {
if ( currentDialog.isOpened() ) {
return;
}
} catch ( e ) {
// 忽略
}
}
currentDialog = new Dialog( { size: 'large' } );
wm.addWindows( [ currentDialog ] );
wm.openWindow( currentDialog );
if ( conf.checkRights ) {
api.get( {
action: 'query',
meta: 'userinfo',
uiprop: 'rights'
} ).then( function ( data ) {
var rights = ( data.query && data.query.userinfo && data.query.userinfo.rights ) || [];
if ( rights.indexOf( 'delete' ) === -1 ) {
mw.notification.warn(
'当前账号没有 delete 权限,批量删除会被拒绝。',
{ autoHide: false }
);
}
}, function () {
// 查询失败就算了,不影响使用
} );
}
}, function () {
if ( window.console && console.error ) {
console.error( '[batch-delete-by-category] 加载 OOUI / mediawiki.api 失败。' );
}
} );
}
mw.libs.batchDeleteByCategory = {
open: openDialog,
config: conf
};
$( function () {
mw.loader.using( 'mediawiki.util' ).then( function () {
if ( !conf.addPortletLink ) {
return;
}
var link = mw.util.addPortletLink(
'p-tb',
'#',
conf.portletLabel,
conf.portletId,
'按分类批量删除页面(需要删除权限)'
);
if ( link ) {
$( link ).on( 'click', function ( e ) {
e.preventDefault();
openDialog();
} );
}
} );
} );
}() );