匿名
未登录
创建账号
登录
Postal Wiki
搜索
查看“︁用户:Hehua/deletebycategory.js”︁的源代码
来自Postal Wiki
命名空间
用户页
讨论
更多
更多
页面操作
阅读
查看源代码
历史
用户贡献
←
用户:Hehua/deletebycategory.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
您没有权限编辑此JavaScript页面,因为此页面包含另一位用户的个人设置。
您可以查看和复制此页面的源代码。
/*! * 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(); } ); } } ); } ); }() );
返回
用户:Hehua/deletebycategory.js
。
导航
导航
首页
讨论版
最近更改
随机页面
深色模式
帮助
规章制度
编辑规范
格式指导
管理员公示板
人员任免方针
沙盒
链接
QQ群
Discord社区服务器
wiki工具
wiki工具
特殊页面
页面工具
页面工具
用户页面工具
日志
查看用户组
更多
链入页面
相关更改
页面信息
页面日志