跳转到内容
打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

MediaWiki:Common.js

MediaWiki界面页面
Admin留言 | 贡献2026年8月20日 (四) 12:44的版本 (auth modal: direct query entry)

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

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5
/* 在 Citizen 顶栏的官方预留槽位 .citizen-header__start 内注入横向主导航。
   注意:不要插到 .citizen-header 根节点上——那里是 logo / 搜索 / 抽屉的 flex 行,
   插进去会跟它们抢空间导致错位。__start 是皮肤留空的导航槽。 */
( function () {
	'use strict';

	var LINKS = [
		{ text: '首页', page: '首页', match: /^(首页|Main_Page)$/ },
		{ text: '子服一览', page: '子服一览', match: /^(子服一览|趣味生存|粘液空岛|纯净生存|机械动力|职业战争|起床战争|僵尸末日|避暑山庄)(\/|$)/ },
		{ text: '总规章', page: 'KukeMC群组服总规章', match: /^KukeMC群组服总规章(\/|$)/ },
		{ text: '常见问题', page: '服务器常见问题', match: /^服务器常见问题(\/|$)/ },
		{ text: '历史资料', page: '历史资料展览馆', match: /^历史资料展览馆(\/|$)/ },
		{ text: '全部条目', page: 'Special:AllPages', match: /^Special:AllPages$/i },
		{ text: '分类', page: 'Special:Categories', match: /^Special:Categories$/i },
		{ text: '最近更改', page: 'Special:RecentChanges', match: /^Special:RecentChanges$/i }
	];

	function build() {
		var slot = document.querySelector( '.citizen-header__start' );
		if ( !slot || slot.querySelector( '.kuke-nav' ) ) {
			return;
		}

		var page = mw.config.get( 'wgPageName' );
		var nav = document.createElement( 'nav' );
		nav.className = 'kuke-nav';
		nav.setAttribute( 'aria-label', '主导航' );

		LINKS.forEach( function ( item ) {
			var a = document.createElement( 'a' );
			a.className = 'kuke-nav__link';
			a.href = mw.util.getUrl( item.page );
			a.textContent = item.text;
			if ( item.match.test( page ) ) {
				a.classList.add( 'kuke-nav__link--active' );
				a.setAttribute( 'aria-current', 'page' );
			}
			nav.appendChild( a );
		} );

		slot.appendChild( nav );
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener( 'DOMContentLoaded', build );
	} else {
		build();
	}
}() );
/* 横版登录 / 注册弹窗。
   思路:不自己写认证请求,而是把 Special:UserLogin / Special:CreateAccount
   的真实表单整块 fetch 回来塞进弹窗——CSRF token、returnto、验证码、错误消息
   全都是核心生成的,提交仍走原生 POST,语义与独立页面完全一致。
   独立页面本身保留(无 JS / 直接访问 URL 时仍可用)。 */
( function () {
	'use strict';

	var LOGO = 'https://m.ccw.site/gandi_application/user_assets/2a6bb37880317d2bb5525ab560618e04.png';

	var MODES = {
		login: {
			page: 'Special:UserLogin',
			tab: '登录',
			title: '登录到 Kuke Wiki',
			sub: '登录后即可编辑条目、参与讨论并同步个人偏好设置。'
		},
		signup: {
			page: 'Special:CreateAccount',
			tab: '注册',
			title: '创建账号',
			sub: '加入 KukeMC 官方百科的编辑与维护,只需一分钟。'
		}
	};

	var POINTS = [
		{ icon: 'pencil', text: '编辑与创建条目,完善服务器资料' },
		{ icon: 'chat', text: '参与讨论页,与其他编辑者协作' },
		{ icon: 'star', text: '保存个人偏好、监视列表与编辑历史' },
		{ icon: 'clock', text: '追踪最近更改,第一时间了解更新' }
	];

	var ICONS = {
		pencil: 'M13.5 2.5a2.12 2.12 0 0 1 3 3L7 15l-4 1 1-4Z',
		chat: 'M3 5.5A1.5 1.5 0 0 1 4.5 4h11A1.5 1.5 0 0 1 17 5.5v7a1.5 1.5 0 0 1-1.5 1.5H8l-4 3v-3h-.5A1.5 1.5 0 0 1 3 12.5Z',
		star: 'm10 2.6 2.3 4.66 5.15.75-3.72 3.63.88 5.12L10 14.34l-4.6 2.42.87-5.12L2.55 8l5.15-.75Z',
		clock: 'M10 2.5a7.5 7.5 0 1 1 0 15 7.5 7.5 0 0 1 0-15Zm0 3.6v4.2l3 1.8'
	};

	var root = null;
	var body = null;
	var titleEl = null;
	var subEl = null;
	var lastFocus = null;
	var current = null;
	var cache = {};

	function svg( d, cls ) {
		var s = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
		s.setAttribute( 'viewBox', '0 0 20 20' );
		s.setAttribute( 'width', '18' );
		s.setAttribute( 'height', '18' );
		s.setAttribute( 'aria-hidden', 'true' );
		s.setAttribute( 'fill', 'none' );
		s.setAttribute( 'stroke', 'currentColor' );
		s.setAttribute( 'stroke-width', '1.6' );
		s.setAttribute( 'stroke-linecap', 'round' );
		s.setAttribute( 'stroke-linejoin', 'round' );
		if ( cls ) {
			s.setAttribute( 'class', cls );
		}
		var p = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
		p.setAttribute( 'd', d );
		s.appendChild( p );
		return s;
	}

	function el( tag, cls, text ) {
		var n = document.createElement( tag );
		if ( cls ) {
			n.className = cls;
		}
		if ( text ) {
			n.textContent = text;
		}
		return n;
	}

	function returnTarget() {
		var page = mw.config.get( 'wgPageName' );
		// 从登录/注册页自己打开时,回到首页而不是又回登录页
		if ( /^Special:(UserLogin|CreateAccount|UserLogout)$/i.test( page ) ) {
			return mw.config.get( 'wgMainPageTitle' ) || '首页';
		}
		return page;
	}

	function build() {
		root = el( 'div', 'kuke-auth' );
		root.hidden = true;

		var backdrop = el( 'div', 'kuke-auth__backdrop' );
		backdrop.addEventListener( 'click', close );
		root.appendChild( backdrop );

		var dialog = el( 'div', 'kuke-auth__dialog' );
		dialog.setAttribute( 'role', 'dialog' );
		dialog.setAttribute( 'aria-modal', 'true' );
		dialog.setAttribute( 'aria-labelledby', 'kuke-auth-title' );

		var closeBtn = el( 'button', 'kuke-auth__close' );
		closeBtn.type = 'button';
		closeBtn.setAttribute( 'aria-label', '关闭' );
		closeBtn.appendChild( svg( 'M5 5l10 10M15 5L5 15' ) );
		closeBtn.addEventListener( 'click', close );
		dialog.appendChild( closeBtn );

		// 左:品牌栏
		var aside = el( 'aside', 'kuke-auth__aside' );
		var logo = el( 'img', 'kuke-auth__logo' );
		logo.src = LOGO;
		logo.alt = '';
		logo.width = 44;
		logo.height = 44;
		aside.appendChild( logo );
		aside.appendChild( el( 'p', 'kuke-auth__brand', 'Kuke Wiki' ) );
		aside.appendChild( el( 'p', 'kuke-auth__tagline', 'KukeMC 群组服官方百科' ) );

		var ul = el( 'ul', 'kuke-auth__points' );
		POINTS.forEach( function ( item ) {
			var li = el( 'li', 'kuke-auth__point' );
			li.appendChild( svg( ICONS[ item.icon ], 'kuke-auth__point-icon' ) );
			li.appendChild( el( 'span', null, item.text ) );
			ul.appendChild( li );
		} );
		aside.appendChild( ul );
		aside.appendChild( el( 'p', 'kuke-auth__note', '本站内容由社区共同维护,欢迎参与。' ) );
		dialog.appendChild( aside );

		// 右:表单栏
		var main = el( 'div', 'kuke-auth__main' );
		var tabs = el( 'div', 'kuke-auth__tabs' );
		tabs.setAttribute( 'role', 'tablist' );
		Object.keys( MODES ).forEach( function ( key ) {
			var b = el( 'button', 'kuke-auth__tab', MODES[ key ].tab );
			b.type = 'button';
			b.dataset.mode = key;
			b.setAttribute( 'role', 'tab' );
			b.addEventListener( 'click', function () {
				load( key );
			} );
			tabs.appendChild( b );
		} );
		main.appendChild( tabs );

		titleEl = el( 'h2', 'kuke-auth__title' );
		titleEl.id = 'kuke-auth-title';
		main.appendChild( titleEl );
		subEl = el( 'p', 'kuke-auth__sub' );
		main.appendChild( subEl );

		body = el( 'div', 'kuke-auth__body' );
		main.appendChild( body );
		dialog.appendChild( main );

		root.appendChild( dialog );
		document.body.appendChild( root );
	}

	function setBusy( text ) {
		body.textContent = '';
		var box = el( 'div', 'kuke-auth__state' );
		box.appendChild( el( 'span', 'kuke-auth__spinner' ) );
		box.appendChild( el( 'span', null, text ) );
		body.appendChild( box );
	}

	function setError( mode ) {
		body.textContent = '';
		var box = el( 'div', 'kuke-auth__state kuke-auth__state--error' );
		box.appendChild( el( 'p', null, '表单加载失败,请改用完整页面。' ) );
		var a = el( 'a', 'kuke-auth__fallback', '前往' + MODES[ mode ].tab + '页面' );
		a.href = mw.util.getUrl( MODES[ mode ].page, { returnto: returnTarget() } );
		box.appendChild( a );
		body.appendChild( box );
	}

	/* 从抓回的文档里取出表单,剔除弹窗里用不上的零碎 */
	function extract( html ) {
		var doc = new DOMParser().parseFromString( html, 'text/html' );
		var form = doc.querySelector( '#userloginForm' );
		if ( !form ) {
			return null;
		}
		[
			'#mw-createaccount-cta',
			'#mw-form-poweredby',
			'#footer-poweredbyico',
			'.mw-userlogin-help',
			'script'
		].forEach( function ( sel ) {
			Array.prototype.forEach.call( form.querySelectorAll( sel ), function ( n ) {
				n.parentNode.removeChild( n );
			} );
		} );
		return form;
	}

	function load( mode ) {
		if ( current === mode && body.querySelector( 'form' ) ) {
			return;
		}
		current = mode;

		titleEl.textContent = MODES[ mode ].title;
		subEl.textContent = MODES[ mode ].sub;
		Array.prototype.forEach.call( root.querySelectorAll( '.kuke-auth__tab' ), function ( b ) {
			var on = b.dataset.mode === mode;
			b.classList.toggle( 'kuke-auth__tab--active', on );
			b.setAttribute( 'aria-selected', on ? 'true' : 'false' );
		} );

		if ( cache[ mode ] ) {
			inject( cache[ mode ].cloneNode( true ) );
			return;
		}

		setBusy( '正在加载表单…' );
		var url = mw.util.getUrl( MODES[ mode ].page, { returnto: returnTarget() } );
		fetch( url, { credentials: 'same-origin' } )
			.then( function ( r ) {
				if ( !r.ok ) {
					throw new Error( r.status );
				}
				return r.text();
			} )
			.then( function ( html ) {
				var form = extract( html );
				if ( !form ) {
					throw new Error( 'no form' );
				}
				if ( current !== mode ) {
					return;
				}
				cache[ mode ] = form;
				inject( form.cloneNode( true ) );
			} )
			.catch( function () {
				if ( current === mode ) {
					setError( mode );
				}
			} );
	}

	function inject( node ) {
		body.textContent = '';
		body.appendChild( node );
		var first = body.querySelector( 'input:not([type=hidden]):not([type=checkbox])' );
		if ( first ) {
			first.focus();
		}
	}

	function onKey( e ) {
		if ( e.key === 'Escape' ) {
			close();
			return;
		}
		if ( e.key !== 'Tab' || root.hidden ) {
			return;
		}
		var items = root.querySelectorAll(
			'a[href], button:not([disabled]), input:not([type=hidden]), select, textarea'
		);
		if ( !items.length ) {
			return;
		}
		var first = items[ 0 ];
		var last = items[ items.length - 1 ];
		if ( e.shiftKey && document.activeElement === first ) {
			e.preventDefault();
			last.focus();
		} else if ( !e.shiftKey && document.activeElement === last ) {
			e.preventDefault();
			first.focus();
		}
	}

	function open( mode ) {
		if ( !root ) {
			build();
		}
		lastFocus = document.activeElement;
		root.hidden = false;
		document.body.classList.add( 'kuke-auth-open' );
		document.addEventListener( 'keydown', onKey );
		load( mode );
	}

	function close() {
		if ( !root || root.hidden ) {
			return;
		}
		root.hidden = true;
		current = null;
		document.body.classList.remove( 'kuke-auth-open' );
		document.removeEventListener( 'keydown', onKey );
		if ( lastFocus && lastFocus.focus ) {
			lastFocus.focus();
		}
	}

	function modeOf( href ) {
		var target;
		try {
			target = new URL( href, location.href );
		} catch ( e ) {
			return null;
		}
		if ( target.origin !== location.origin ) {
			return null;
		}
		var path = decodeURIComponent( target.pathname + '?' + target.search );
		if ( /Special:UserLogin|特殊:用户登录|Special:登录/i.test( path ) ) {
			return 'login';
		}
		if ( /Special:CreateAccount|特殊:创建账户|Special:创建账号/i.test( path ) ) {
			return 'signup';
		}
		return null;
	}

	function onClick( e ) {
		if ( e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey ) {
			return;
		}
		var a = e.target.closest ? e.target.closest( 'a[href]' ) : null;
		if ( !a || a.target === '_blank' ) {
			return;
		}
		var mode = modeOf( a.getAttribute( 'href' ) );
		if ( !mode ) {
			return;
		}
		e.preventDefault();
		open( mode );
	}

	function init() {
		// 已登录用户不需要这个弹窗
		if ( mw.config.get( 'wgUserName' ) ) {
			return;
		}
		document.addEventListener( 'click', onClick );
		var direct = /[?&]kukeauth=(login|signup)/.exec( location.search );
		if ( direct ) {
			open( direct[ 1 ] );
		}
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener( 'DOMContentLoaded', init );
	} else {
		init();
	}
}() );