首页
社区
新闻
视频博客
锦标赛
排行榜
接受挑战
维基
论坛
navigation
首页
最近更改
随机页面
MediaWiki帮助
TOOLBOX
链入页面
相关更改
特殊页面
页面信息
cn
RU
EN
DE
个人工具
登录
查看“︁MediaWiki:BatchUpload.js”︁的源代码
来自3D坦克-WIKI
←
MediaWiki:BatchUpload.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
/* * MediaWiki 批量上传页面 v1.0 * 将本文件全文保存到 MediaWiki:BatchUpload.js(不要加 <script> 标签)。 * 入口:Project:批量上传;加载片段见 Common.js.snippet.js。 * 依赖:MediaWiki 自带 mediawiki.api / mediawiki.util / jQuery;现代浏览器。 * 不使用外部服务、不保存密码、不请求忽略上传警告。 */ (function () { 'use strict'; var CONFIG = { namespace: 4, pageTitle: '批量上传', containerId: 'mw-batch-upload', maxFiles: 200, // 本地队列上限,不是服务器限制 intervalMs: 500 // 两个文件之间的间隔;上传始终串行 }; if (mw.config.get('wgNamespaceNumber') !== CONFIG.namespace || mw.config.get('wgTitle') !== CONFIG.pageTitle || mw.config.get('wgAction') !== 'view' || mw.config.get('wgDiffNewId')) { return; } mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () { jQuery(function () { var root = document.getElementById(CONFIG.containerId); if (!root || root.dataset.batchUploadReady) { return; } root.dataset.batchUploadReady = '1'; mount(root); }); }, function () { jQuery(function () { var root = document.getElementById(CONFIG.containerId); if (root) { root.textContent = '无法加载 MediaWiki API 模块。请强制刷新,或联系界面管理员。'; } }); }); function mount(root) { mw.util.addCSS([ '#mw-batch-upload{--bu-border:var(--border-color-subtle,#c8ccd1);color:var(--color-base,#202122);font-size:14px;line-height:1.6;margin:1em 0}', '#mw-batch-upload *{box-sizing:border-box}', '#mw-batch-upload .bu-panel{border:1px solid var(--bu-border);border-radius:8px;padding:20px;background:var(--background-color-base,#fff)}', '#mw-batch-upload .bu-heading{font-size:21px;font-weight:700;margin:0 0 6px}', '#mw-batch-upload .bu-note{color:var(--color-subtle,#54595d);margin:6px 0 12px;font-size:13px}', '#mw-batch-upload .bu-drop{padding:24px 16px;text-align:center;border:2px dashed var(--bu-border);border-radius:6px;margin:16px 0;background:var(--background-color-interactive-subtle,#f8f9fa)}', '#mw-batch-upload .bu-drop.bu-over{border-color:#36c;background:#eaf3ff;color:#202122}', '#mw-batch-upload .bu-fields{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:14px}', '#mw-batch-upload label{display:block;font-weight:600}', '#mw-batch-upload input[type=text],#mw-batch-upload textarea{width:100%;padding:8px;border:1px solid var(--bu-border);border-radius:4px;color:inherit;background:var(--background-color-base,#fff);font:inherit;font-weight:400}', '#mw-batch-upload textarea{min-height:100px;resize:vertical}', '#mw-batch-upload .bu-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0 16px}', '#mw-batch-upload .bu-inline input{flex:1;min-width:140px}', '#mw-batch-upload button{padding:7px 13px;border:1px solid var(--bu-border);border-radius:4px;font:inherit;font-weight:600;cursor:pointer;background:var(--background-color-interactive,#eaecf0);color:inherit}', '#mw-batch-upload button.bu-primary{background:#36c;border-color:#36c;color:#fff}', '#mw-batch-upload button:disabled{opacity:.5;cursor:not-allowed}', '#mw-batch-upload button:focus-visible,#mw-batch-upload input:focus-visible,#mw-batch-upload textarea:focus-visible{outline:2px solid #36c;outline-offset:2px}', '#mw-batch-upload .bu-toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}', '#mw-batch-upload .bu-message{padding:10px 12px;margin:10px 0;border-left:3px solid #36c;background:var(--background-color-interactive-subtle,#f8f9fa);white-space:pre-wrap;overflow-wrap:anywhere}', '#mw-batch-upload .bu-scroll{overflow-x:auto}', '#mw-batch-upload table{width:100%;min-width:630px;border-collapse:collapse;margin-top:12px}', '#mw-batch-upload th,#mw-batch-upload td{padding:10px 8px;border-bottom:1px solid var(--bu-border);text-align:left;vertical-align:top}', '#mw-batch-upload th{font-weight:600;background:var(--background-color-interactive-subtle,#f8f9fa)}', '#mw-batch-upload .bu-original{width:23%;overflow-wrap:anywhere}', '#mw-batch-upload .bu-target{width:31%}', '#mw-batch-upload .bu-result{width:36%;overflow-wrap:anywhere}', '#mw-batch-upload .bu-size{font-size:12px;color:var(--color-subtle,#54595d)}', '#mw-batch-upload .bu-status{white-space:pre-wrap}', '#mw-batch-upload tr[data-state=success] .bu-status{color:var(--color-success,#14866d)}', '#mw-batch-upload tr[data-state=warning] .bu-status{color:var(--color-warning,#8b5a00)}', '#mw-batch-upload tr[data-state=failed] .bu-status{color:var(--color-error,#b32424)}', '#mw-batch-upload progress{display:block;width:100%;height:8px;accent-color:#36c;margin:7px 0}', '#mw-batch-upload .bu-empty{text-align:center;color:var(--color-subtle,#54595d);padding:24px}', '#mw-batch-upload [hidden]{display:none!important}', '@media(max-width:650px){#mw-batch-upload .bu-fields{grid-template-columns:1fr}#mw-batch-upload .bu-panel{padding:14px}}' ].join('\n')); // HTML 全部为静态字符串。文件名、API 错误、用户输入只以 textContent/value 写入。 root.innerHTML = ` <section class="bu-panel" aria-label="批量上传文件"> <div class="bu-heading">批量上传文件</div> <p class="bu-note">上传到当前 Wiki,使用当前账号的权限。遇到警告不自动确认,不主动覆盖已有文件。</p> <div class="bu-message" data-el="message" role="status" aria-live="polite">正在检查登录状态和上传权限……</div> <div class="bu-drop" data-el="drop"> <button type="button" data-el="choose" disabled>选择多个文件</button> <span> 或将文件拖到这里</span> <input type="file" multiple hidden data-el="files" aria-label="选择待上传文件" disabled> <div class="bu-note">可以分多次添加。类型和大小由本站设置决定;本工具不支持目录或分片上传。</div> </div> <div class="bu-fields"> <label>统一文件说明(维基语法) <textarea data-el="description" placeholder="填写来源、作者和授权说明。若使用许可模板,请填写本站已有、且确实适用的模板。" disabled></textarea> </label> <label>分类(可选,每行一个名称) <textarea data-el="categories" placeholder="活动照片 2026年资料" disabled></textarea> </label> </div> <label for="bu-prefix">文件名前缀(可选)</label> <div class="bu-inline"> <input id="bu-prefix" data-el="prefix" type="text" placeholder="例如:项目A-" disabled> <button type="button" data-el="applyPrefix" disabled>应用到未成功项</button> </div> <p class="bu-note">应用前缀会按“前缀 + 原始文件名”重建未成功项的名称,替换手工改名;不会重复叠加前缀。请保留正确的扩展名。</p> <div class="bu-toolbar"> <button type="button" class="bu-primary" data-el="start" disabled>开始上传</button> <button type="button" data-el="pause" disabled>本项完成后暂停</button> <button type="button" data-el="retry" disabled>重试失败 / 警告项</button> <button type="button" data-el="clear" disabled>清空列表</button> </div> <div data-el="summary" role="status" aria-live="polite">尚未选择文件</div> <progress data-el="overall" max="1" value="0" aria-label="处理进度(按文件数量)"></progress> <div class="bu-scroll"> <table> <thead><tr><th scope="col">本地文件</th><th scope="col">目标文件名</th><th scope="col">状态 / 传输进度</th><th scope="col">操作</th></tr></thead> <tbody data-el="rows"></tbody> </table> <div class="bu-empty" data-el="empty">选择文件后,会在这里显示上传队列。</div> </div> <p class="bu-note">进度条表示传输或已处理数量,不代表上传成功;以“成功”状态为准。暂停不会中断正在处理的文件。清空列表不会删除站内文件。</p> </section>`; var ui = {}; root.querySelectorAll('[data-el]').forEach(function (el) { ui[el.dataset.el] = el; }); var items = []; var busy = false; var pauseRequested = false; var ready = false; var userName = mw.config.get('wgUserName'); var api; var fileNamespace = (mw.config.get('wgFormattedNamespaces') || {})[6] || 'File'; var categoryNamespace = (mw.config.get('wgFormattedNamespaces') || {})[14] || 'Category'; function message(text) { ui.message.textContent = text; } function fileUrl(name) { return mw.util.getUrl(fileNamespace + ':' + name); } function sleep(ms) { return new Promise(function (resolve) { window.setTimeout(resolve, ms); }); } function sizeText(bytes) { if (bytes < 1024) { return bytes + ' B'; } if (bytes < 1048576) { return (bytes / 1024).toFixed(1) + ' KiB'; } return (bytes / 1048576).toFixed(1) + ' MiB'; } function counts() { var result = { queued: 0, uploading: 0, success: 0, warning: 0, failed: 0 }; items.forEach(function (item) { result[item.state]++; }); return result; } function refresh() { var c = counts(); var locked = busy || !ready; ['choose', 'files', 'description', 'categories', 'prefix'].forEach(function (key) { ui[key].disabled = locked; }); ui.applyPrefix.disabled = locked || !items.some(function (item) { return item.state !== 'success'; }); ui.start.disabled = locked || !c.queued; ui.pause.disabled = !busy || pauseRequested; ui.retry.disabled = locked || !(c.failed + c.warning); ui.clear.disabled = busy || !items.length; ui.empty.hidden = items.length > 0; ui.overall.max = Math.max(items.length, 1); ui.overall.value = c.success + c.warning + c.failed; ui.summary.textContent = items.length ? '共 ' + items.length + ' 项 · 成功 ' + c.success + ' · 警告 ' + c.warning + ' · 失败 ' + c.failed + ' · 待处理 ' + c.queued + (busy ? ' · 正在处理' : '') : '尚未选择文件'; items.forEach(function (item) { item.input.disabled = locked || item.state === 'success'; item.remove.disabled = busy; }); } function setState(item, state, text) { item.state = state; item.row.dataset.state = state; item.status.textContent = text; item.progress.hidden = state !== 'uploading' && state !== 'success'; if (state === 'queued') { item.progress.value = 0; } if (state === 'success') { item.progress.value = 100; } item.link.hidden = state !== 'success' && state !== 'warning' && state !== 'failed'; item.link.href = fileUrl(item.input.value.trim()); item.link.textContent = state === 'success' ? '查看文件' : '检查目标页面'; refresh(); } function resetItem(item) { if (item.state !== 'success') { setState(item, 'queued', '等待上传'); } } function addFiles(files) { if (busy || !ready) { return; } var added = 0; var excess = 0; Array.from(files).forEach(function (file) { if (items.length >= CONFIG.maxFiles) { excess++; return; } var tr = document.createElement('tr'); tr.dataset.state = 'queued'; tr.innerHTML = '<td class="bu-original"><div class="bu-name"></div><div class="bu-size"></div></td>' + '<td class="bu-target"><input type="text"></td>' + '<td class="bu-result"><div class="bu-status"></div><progress max="100" value="0" hidden></progress><a target="_blank" rel="noopener noreferrer" hidden></a></td>' + '<td><button type="button" title="只移出队列,不删除站内文件">移除</button></td>'; tr.querySelector('.bu-name').textContent = file.name; tr.querySelector('.bu-size').textContent = sizeText(file.size); var item = { file: file, state: 'queued', row: tr, input: tr.querySelector('input'), status: tr.querySelector('.bu-status'), progress: tr.querySelector('progress'), link: tr.querySelector('a'), remove: tr.querySelector('button') }; item.input.value = ui.prefix.value + file.name; item.input.setAttribute('aria-label', file.name + ' 的目标文件名'); item.progress.setAttribute('aria-label', file.name + ' 的传输进度'); item.status.textContent = '等待上传'; item.input.addEventListener('input', function () { resetItem(item); }); item.remove.addEventListener('click', function () { if (busy) { return; } items = items.filter(function (other) { return other !== item; }); tr.remove(); refresh(); }); items.push(item); ui.rows.appendChild(tr); added++; }); message('已添加 ' + added + ' 个文件。请检查文件名、来源和授权说明后点击“开始上传”。' + (excess ? '\n另有 ' + excess + ' 个文件未添加:本地队列上限为 ' + CONFIG.maxFiles + ' 个。' : '')); refresh(); } // 将 mw.Api 的多参数拒绝转换为 Error;不丢失服务器的错误信息。 function apiError(code, data) { var raw = data && (data.error || (data.errors && data.errors[0])); var text = raw && (raw.info || raw.text || raw['*']); var status = data && data.xhr && data.xhr.status; if (code === 'http') { text = '网络 / HTTP 异常' + (status ? '(HTTP ' + status + ')' : '') + '。若上传请求已发出,结果可能未确认;请先检查目标页面,再决定是否重试。'; } var error = new Error(text || String(code || '未知 API 错误')); error.code = String(code || 'unknown'); error.stopQueue = code === 'http' || /assert|badtoken|notloggedin|read.?only|upload.*disabled|ratelimited|maxlag|permissiondenied|blocked/i.test(error.code); return error; } function request(jqPromise) { return new Promise(function (resolve, reject) { jqPromise.done(resolve).fail(function (code, data) { reject(apiError(code, data)); }); }); } function warningError(text) { var error = new Error(text); error.isWarning = true; return error; } function warningText(warnings) { var labels = { exists: '同名文件已存在', 'exists-normalized': '规范化后的文件名已存在', 'page-exists': '同名文件说明页已存在', duplicate: '相同内容的文件已存在', 'no-change': '内容与现有文件一致', duplicateversions: '内容与已有历史版本一致', 'was-deleted': '这个文件名曾被删除', 'duplicate-archive': '相同内容的文件曾被删除', badfilename: '文件名需要调整', 'bad-prefix': '文件名前缀不符合站点要求', 'large-file': '文件大小触发站点警告' }; return Object.keys(warnings || {}).map(function (key) { var value = warnings[key]; var detail = typeof value === 'string' ? value : JSON.stringify(value); return (labels[key] || '站点警告') + ' [' + key + ']' + (detail ? ':' + detail.slice(0, 500) : ''); }).join('\n') || '服务器返回上传警告。'; } function pageText() { var categories = []; ui.categories.value.split(/\r?\n/).forEach(function (line) { var name = line.trim(); if (!name) { return; } [categoryNamespace, 'Category', '分类', '分類'].forEach(function (ns) { if (name.slice(0, ns.length + 1).toLowerCase() === (ns + ':').toLowerCase()) { name = name.slice(ns.length + 1).trim(); } }); if (!name || /[\[\]{}|<>#\u0000-\u001f\u007f]/.test(name)) { throw new Error('分类请每行只填一个名称,不要填写 [[Category:…]] 等维基代码。'); } if (categories.indexOf(name) === -1) { categories.push(name); } }); var text = ui.description.value.trim(); if (categories.length) { text += (text ? '\n\n' : '') + categories.map(function (name) { return '[[' + categoryNamespace + ':' + name + ']]'; }).join('\n'); } return text; } async function uploadOne(item, text) { var name = item.input.value.trim(); if (!name || /[\\/:#<>\[\]|{}\u0000-\u001f\u007f]/.test(name) || !/\.[^.\s]+$/.test(name)) { throw new Error('目标名称需包含扩展名,且不能包含路径、命名空间或非法字符(如 / : # [ ])。'); } if (!item.file.size) { throw new Error('文件为空,或浏览器无法读取。请使用文件选择器选择普通文件,不要拖入文件夹。'); } item.input.value = name; setState(item, 'uploading', '正在检查目标文件名……'); item.progress.removeAttribute('value'); // 同时检查本地文件说明页及共享仓库文件;避免无意遮蔽共享文件。 var check = await request(api.get({ action: 'query', titles: fileNamespace + ':' + name, prop: 'imageinfo', iiprop: 'timestamp' })); var page = check.query && check.query.pages && check.query.pages[0]; if (!page || page.invalid || page.ns !== 6) { throw new Error('服务器不接受这个文件名,或未能返回有效的文件页面信息。'); } if (!page.missing || (page.imageinfo && page.imageinfo.length) || (page.imagerepository && page.imagerepository !== '')) { throw warningError('目标文件或文件说明页已存在(可能来自共享仓库)。未发送文件;请改名或使用已有文件。'); } // 使用服务器规范化的首字母、空格与名称。 name = page.title.slice(page.title.indexOf(':') + 1); item.input.value = name; item.progress.value = 0; item.status.textContent = '正在获取令牌并上传……'; var result = await request(api.postWithToken('csrf', { action: 'upload', filename: name, file: item.file, text: text, comment: '使用批量上传页面上传', watchlist: 'nochange' // 刻意不传 ignorewarnings,也不根据 filekey 自动确认警告。 }, { contentType: 'multipart/form-data', // mw.Api 会构造 FormData 并设置 boundary timeout: 0, xhr: function () { var xhr = jQuery.ajaxSettings.xhr(); if (xhr.upload) { xhr.upload.addEventListener('progress', function (event) { if (event.lengthComputable) { var percent = Math.min(100, Math.round(event.loaded / event.total * 100)); item.progress.value = percent; item.status.textContent = percent < 100 ? '正在传输 ' + percent + '%' : '传输完成,等待服务器确认……'; } }); } return xhr; } })); var upload = result && result.upload; // 只以 API 的 Success 为成功;HTTP 200 或传输到 100% 都不是成功确认。 if (upload && upload.result === 'Success') { item.input.value = upload.filename || name; setState(item, 'success', '成功'); return; } if (upload && (upload.result === 'Warning' || upload.warnings)) { throw warningError(warningText(upload.warnings) + '\n本工具没有确认警告,未完成正式发布。'); } var unknown = new Error('服务器返回了未识别的上传结果。请先检查目标文件页面,不要直接重复上传。'); unknown.stopQueue = true; throw unknown; } async function run() { if (busy || !ready || !items.some(function (item) { return item.state === 'queued'; })) { return; } var text; try { text = pageText(); } catch (error) { message(error.message); return; } busy = true; pauseRequested = false; refresh(); message('正在逐个上传。请保持页面打开;“本项完成后暂停”不会中断已发出的请求。'); try { for (var i = 0; i < items.length; i++) { if (pauseRequested) { break; } var item = items[i]; if (item.state !== 'queued') { continue; } try { await uploadOne(item, text); } catch (error) { setState(item, error.isWarning ? 'warning' : 'failed', (error.message || String(error)) + (error.code ? ' [' + error.code + ']' : '')); if (error.stopQueue) { pauseRequested = true; } } if (!pauseRequested && i < items.length - 1) { await sleep(CONFIG.intervalMs); } } } finally { busy = false; refresh(); message(pauseRequested ? '队列已暂停。当前请求已返回,未处理项仍在队列中。请检查错误或账号状态,再点击“开始上传”继续;失败项需手动重试。' : '本轮处理结束。请检查各项状态;成功项不会被本工具再次上传。警告项未自动确认,失败项可在排查后重试。'); } } ui.choose.addEventListener('click', function () { ui.files.click(); }); ui.files.addEventListener('change', function () { addFiles(ui.files.files); ui.files.value = ''; }); ['dragenter', 'dragover'].forEach(function (type) { ui.drop.addEventListener(type, function (event) { event.preventDefault(); if (ready && !busy) { ui.drop.classList.add('bu-over'); } }); }); ui.drop.addEventListener('dragleave', function (event) { if (!ui.drop.contains(event.relatedTarget)) { ui.drop.classList.remove('bu-over'); } }); ui.drop.addEventListener('drop', function (event) { event.preventDefault(); ui.drop.classList.remove('bu-over'); if (event.dataTransfer) { addFiles(event.dataTransfer.files); } }); ui.applyPrefix.addEventListener('click', function () { if (busy || !ready) { return; } items.forEach(function (item) { if (item.state !== 'success') { item.input.value = ui.prefix.value + item.file.name; resetItem(item); } }); message('已按“前缀 + 原始文件名”更新未成功项。请检查后再开始上传。'); }); ui.start.addEventListener('click', function () { run(); }); ui.pause.addEventListener('click', function () { pauseRequested = true; message('将在当前文件处理完成后暂停;当前请求不会中止。'); refresh(); }); ui.retry.addEventListener('click', function () { if (busy || !ready) { return; } items.forEach(function (item) { if (item.state === 'failed' || item.state === 'warning') { resetItem(item); } }); run(); }); ui.clear.addEventListener('click', function () { if (busy) { return; } items = []; ui.rows.textContent = ''; message('已清空本地列表。此前上传到本站的文件不会被删除。'); refresh(); }); window.addEventListener('beforeunload', function (event) { if (busy || items.some(function (item) { return item.state !== 'success'; })) { event.preventDefault(); event.returnValue = ''; // 浏览器显示自己的离开页面提示文字 } }); if (!window.File || !window.FormData || !window.Promise) { message('浏览器不支持所需的文件上传功能,请使用较新的 Chrome、Firefox、Edge 或 Safari。'); return; } if (!userName) { message('请先登录,再打开此页面。本工具不提供匿名上传。'); var login = document.createElement('a'); login.href = mw.util.getUrl('Special:UserLogin', { returnto: mw.config.get('wgPageName').replace(/_/g, ' ') }); login.textContent = ' 前往登录'; ui.message.appendChild(login); return; } api = new mw.Api({ parameters: { formatversion: 2, errorformat: 'plaintext', assert: 'user', assertuser: userName, maxlag: 5 } }); request(api.get({ action: 'query', meta: 'userinfo', uiprop: 'rights' })).then(function (data) { var info = data.query && data.query.userinfo; if (!info || !Array.isArray(info.rights)) { throw new Error('未能确认账号的上传权限,请刷新或联系管理员。'); } if (info.rights.indexOf('upload') === -1) { message('当前账号没有 upload 权限。界面脚本不能授予上传权限,请联系站点管理员。'); return; } ready = true; message('当前账号:' + userName + '。请选择文件。建议先用 2~3 个小文件测试。'); refresh(); }).catch(function (error) { message('初始化失败:' + error.message + '。请检查登录状态或 API 是否可用,再刷新页面。'); }); } }());
返回
MediaWiki:BatchUpload.js
。
反馈问题
导航菜单
导航
首页
最近更改
随机页面
MediaWiki帮助
提交支持工单
您的支持请求已受理。技术支持团队会尽快与您联系。
搜索
报告 Wiki 中的错误
请
不要
使用此表单申诉封号或封锁处罚。此表单仅用于反馈
与 Wiki 相关
的错误或改进建议。
您的游戏昵称
页面链接
我们已为您自动填写此项,如有误,请从浏览器地址栏复制所需页面的链接并粘贴至此处。
留言内容
请告诉我们您希望修改该页面的哪些内容,或指出其中的错误/不准确之处。可附带截图链接。