LLWiki正在建设中,欢迎加入我们!
MediaWiki:Gadget-uploader-rewrite.js
跳转到导航
跳转到搜索
注意:在保存之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Internet Explorer:按住Ctrl的同时单击刷新,或按Ctrl-F5
- Opera:前往菜单 → 设置(Mac为Opera → Preferences),然后隐私和安全 → 清除浏览数据 → 缓存的图片和文件。
//<nowiki> // 引自[[moegirl:user:東東君/js/uploader.js|moegirl:user:-{東東君}-/js/uploader.js]] "use strict"; var api = new mw.Api(); /** * @param {object} options * @param {File} options.body * @param {string} options.fileName * @param {string} options.comment * @param {string} options.pageContent */ function upload({ body, fileName, comment, pageContent }) { return new Promise(function (resolve, reject) { var data = { filename: fileName, comment: comment, text: pageContent, format: 'json', ignorewarnings: 1 }; if (typeof body == 'string') { data.url = body; data.action = 'upload'; console.log('Upload by url: ' + body); api.postWithToken('csrf', data).then(resolve, reject); } else { api.upload(body, data).then(resolve, reject); } }); } /** * @param {string[]} fileNames */ function checkFileNames(fileNames) { return api.get({ action: 'query', titles: fileNames.map(item => 'File:' + item).join('|'), converttitles: 1, formatversion: 2 }).then(function (data) { var result = {}; data.query.pages.forEach( function(item) { result[item.title.substring(5)] = !('missing' in item); return result; } ); return result; }); } $.when( $.ready, mw.loader.getScript( 'https://unpkg.com/[email protected]/dist/vue.min.js' ) ).then(function() { $(document.body).append('<div id="widget-fileUploader" style="display:none">'); // 向“更多”菜单注入按钮 $('#p-cactions ul').append('<li id="btn-fileUploader"><a title="上传文件">' + wgULS("批量上传文件", "批次上傳檔案") + '</a></li>'); $('#btn-fileUploader').click(() => { $('#widget-fileUploader').fadeIn(200); $('#content').css('position', 'static'); }); const template = ` <div id="widget-fileUploader" class="uploader-container"> <input ref="fileInput" style="display:none" type="file" multiple="multiple" :accept="allowedFileTypes.map(item => '.' + item).join(',')" @change="addFileByFileSelector" /> <div class="uploader-closeBtn" @click="hideWidget">×</div> <div class="uploader-body"> <div class="uploader-fileList" @dragenter.prevent="() => {}" @dragover.prevent="() => {}" @drop.prevent="addFileByDropping"> <div v-if="files.length === 0" key="hintMask" class="hintMask" @click="$refs.fileInput.click()"> <div class="hintText">` + wgULS("点此添加文件,或将文件拖拽至此", "點此添加檔案,或將檔案拖拽至此") + `</div> </div> <div v-for="(item, index) in files" :key="item.body.lastModified" class="item" :data-name="item.fileName" :data-selected="index === focusedFileIndex" title="单击选中文件,双击复制文件名" @click="focusFile(index)"> <img v-if="isImageFile(item.body)" :src="item.objectUrl" /> <div v-else class="unablePreviewHint"> <div>` + wgULS("不支持预览的文件类型", "不支援預覽的檔案類型") + `</div> <div v-if="typeof item.body !== 'string'" class="type">Mimetype: {{ item.body.type }}</div> </div> <div class="removeBtn" @click.stop="files.splice(index, 1)">×</div> </div> <div v-if="files.length !== 0" class="item addFileBox" @click="$refs.fileInput.click()" /> </div> <div class="uploader-panel"> <div class="block"> <div class="input-container" title="上传后使用文件时的名字,要求不能和现有文件重复"> <span>` + wgULS("文件", "檔案") + `名:</span> <input v-model.trim="form.fileName" /> </div> <div class="input-container categoryInput" title="所有文件共享分类"> <span>` + wgULS("分 类", "分 類") + `:</span> <input ref="categoryInput" v-model.trim="form.categoryInput" @input="loadCategoryHint" @keydown.enter="addCategory(form.categoryInput)" @keydown.up.prevent="handlerFor_categoryInput_wasKeyDowned" /> <div class="inputHint">` + wgULS("按下回车键添加分类", "按下確認鍵添加分類") + `</div> <div ref="categoryHints" v-if="categoryHints.length !== 0" class="categoryHints" @keydown.enter="addCategory(categoryHints[categoryHintFocusedIndex])" @keydown.prevent="handlerFor_categoryHints_wasKeyDowned"> <div v-for="(item, index) in categoryHints" class="item" :data-selected="index === categoryHintFocusedIndex" @click="addCategory(item)">{{ item }}</div> </div> </div> <div class="categories"> <div v-for="(item, index) in form.categories" class="item" title="点击删除分类" @click="form.categories.splice(index, 1)" >{{ item }}</div> </div> </div> <div class="block"> <div class="input-container"> <span>角色名:</span> <input v-model.trim="form.charaName" /> </div> <div class="input-container"> <span>` + wgULS("源地址", "原位址") + `:</span> <input v-model.trim="form.source" /> </div> <div class="input-container" style="visibility:hidden"> <span>作 者:</span> <input v-model.trim="form.author" /> </div> </div> <div class="block" style="flex-direction:column; justify-content:space-around; align-items:flex-start;"> <div class="input-container" title="所有文件共享前缀"> <span>` + wgULS("添加前缀", "添加前綴") + `:</span> <input v-model.trim="form.prefix" style="width:calc(100% - 6em)" /> </div> <div class="input-container" style="justify-content:flex-start;"> <select v-model.trim="form.license"> <option disabled="disabled" value="">` + wgULS("选择授权协议(将鼠标放在选项上显示详情)", "選擇授權協定(將滑鼠放在選項上顯示詳情)") + `</option> <optgroup label="CC` + wgULS("协议", "協定") + `"> <option value="CC Zero" title="作者授权以无著作权方式使用">CC-0</option> <option value="CC BY" title="作者授权以署名方式使用,该授权需兼容4.0协议">CC BY 4.0</option> <option value="CC BY-SA" title="作者授权以署名-相同方式方式使用,该授权需兼容4.0协议">CC BY-SA 4.0</option> <option value="CC BY-NC-SA" title="作者授权以署名-非商业使用-相同协议方式使用,该授权需兼容4.0协议">CC BY-NC-SA 4.0</option> </optgroup> <optgroup label="` + wgULS("公有领域", "公有領域") + `">' <option value="PD-Old">` + wgULS("作者离世一定年限后流入公有领域", "作者離世一定年限後流入公有領域") + `</option> <option value="PD-Other">` + wgULS("其他原因流入公有领域", "其他原因流入公有領域") + `</option> </optgroup> <optgroup label="其他"> <option value="Copyright" title="原作者没有明确的授权声明">` + wgULS("原作者保留权利", "原作者保留權利") + `</option> <option value="none:gotoCommons">` + wgULS("原作者授权LLWiki使用", "原作者授權LLWiki使用") + `</option> <option value="可自由使用" title="作者放弃版权或声明可自由使用">可自由使用</option> <option value="LLWiki版权所有">LLWiki` + wgULS("版权所有", "版權所有") + `</option> </optgroup> </select> </div> <div class="buttons"> <button @click="addSourceUrlFile">` + wgULS("添加源地址上传", "添加原始地址上傳") + `</button> <button :disabled="status === 2" title="执行上传文件" @click="submit(false)">` + wgULS("上传", "上傳") + `</button> <button :disabled="status === 2" title="在发生文件名已存在的情况时,自动滤掉已存在的文件。通常用于在上一次批量上传中一部分失败后,再次尝试将之前没传上去的文件重新上传" @click="submit(true)">` + wgULS("差分上传", "差分上傳") + `</button> <button title="将当前文件除文件名的信息同步到全部文件" @click="asyncCurrentFileInfo">同步` + wgULS("文件", "檔案") + `信息</button> <button @click="showManual">` + wgULS("使用说明", "使用說明") + `</button> </div> </div> </div> </div> </div> `; new Vue({ el: '#widget-fileUploader', template, data() { return { allowedFileTypes: ['ogg', 'mp3', 'png', 'gif', 'jpg', 'jpeg', 'webp'], files: [], // 待上传的文件 focusedFileIndex: 0, categoryHints: [], categoryInputDebounceTimeoutKey: 0, categoryHintFocusedIndex: -1, status: 1, // 0:失败,1:初始化,2:提交中,3:成功 form: { fileName: '', categoryInput: '', // 分类输入栏 categories: [], // 实际要提交的分类 charaName: '', author: '', source: '', prefix: '', license: '' }, doubleClickTimeoutKey: 0 // 用于双击复制文件名 }; }, mounted() { $('#widget-fileUploader').hide(); }, watch: { files() { this.focusedFileIndex === 0 && this.focusFile(0); }, form: { deep: true, handler() { if (!this.files[this.focusedFileIndex]) { return; } this.files[this.focusedFileIndex] = { ...this.files[this.focusedFileIndex], fileName: this.form.fileName, author: this.form.author, charaName: this.form.charaName, source: this.form.source, license: this.form.license }; } }, license(val) { if (val === 'none:gotoCommons') { alert(wgULS('该协议需要手动填写授权证明,请到特殊页面上传', '該協定需要手動填寫授權證明,請到特殊頁面上傳')); window.open('https://llwiki.org/zh/Special:上传文件', '_blank'); } } }, computed: { license() { return this.form.license; }, }, methods: { createFileItem(fileBody) { return { body: fileBody, objectUrl: typeof fileBody === 'string' ? fileBody : URL.createObjectURL(fileBody), fileName: typeof fileBody === 'string' ? fileBody.replace(/.+\/(.+?)$/, '$1') : fileBody.name, author: '', charaName: '', source: '', license: 'Copyright' }; }, isImageFile(fileBody) { const imageType = ['jpg', 'png', 'jpeg', 'gif', 'webp']; return imageType.includes( (typeof fileBody === 'string' ? fileBody : fileBody.name).replace(/.+\.(.+?)$/, '$1').toLowerCase() ); }, hideWidget() { $('#widget-fileUploader').fadeOut(200); $('#content').css('position', 'relative'); }, loadCategoryHint() { clearTimeout(this.categoryInputDebounceTimeoutKey); this.categoryInputDebounceTimeoutKey = setTimeout(() => { if (this.form.categoryInput === '') { return; } api.get({action: "query", list: "search", srwhat: 'title', srsearch: this.form.categoryInput, srnamespace: "14", srlimit: "20", srprop: "", formatversion: 2}) .then(data => { const hints = data.query.search.map(item => item.title.substring(9)); this.categoryHints = hints; }); }, 500); }, resetCategory() { this.form.categoryInput = ''; this.categoryHints = []; this.categoryHintFocusedIndex = -1; }, addCategory(categoryName) { this.form.categories.push(categoryName); this.resetCategory(); }, // 实现上下键切换分类提示 handlerFor_categoryHints_wasKeyDowned(e) { if (e.code === 'ArrowUp') { this.categoryHintFocusedIndex++; if (this.categoryHintFocusedIndex > this.categoryHints.length - 1) { this.categoryHintFocusedIndex = 0; } } if (e.code === 'ArrowDown') { this.categoryHintFocusedIndex--; if (this.categoryHintFocusedIndex < 0) { this.$refs.categoryInput.focus(); } } this.categoryHintFocusedIndex >= 0 && this.$refs.categoryHints.querySelectorAll('div')[this.categoryHintFocusedIndex].scrollIntoView(); }, handlerFor_categoryInput_wasKeyDowned() { if (this.categoryHints.length === 0 || !this.$refs.categoryHints) { return; } this.$refs.categoryHints.focus(); this.categoryHintFocusedIndex = 0; }, addFileByFileSelector(e) { const originalFileList = e.target.files; [].forEach.call(originalFileList, file => { if (this.files.length === 50) { return; } if (file.size / 1024 / 1024 > 20) return alert(wgULS("文件", "檔案") + `【${file.name}】` + wgULS("大小超过20MB,无法上传", "大小超過20MB,無法上傳") + `!`); this.files.push(this.createFileItem(file)); }); e.target.value = ''; if (this.files.length === 50) mw.notify(wgULS('一次最多上传50个文件', '一次最多上傳50個檔案'), { type: 'wran' }); }, addFileByDropping(e) { const originalFileList = e.dataTransfer.files; [].forEach.call(originalFileList, file => { if (this.files.length === 50) { return; } if (!this.allowedFileTypes.includes( file.name.replace(/.+\.(.+?)$/, '$1').toLowerCase() )) return alert(`【${file.name}】` + wgULS("不支持上传这种格式的文件", "不支持上傳這種格式的檔案") + `!`); if (file.size / 1024 / 1024 > 20) return alert(`【${file.name}】的` + wgULS("大小超过20MB,无法上传", "大小超過20MB,無法上傳") + `!`); this.files.push(this.createFileItem(file)); }); if (this.files.length === 50) mw.notify(wgULS('一次最多上传50个文件', '一次最多上傳50個檔案'), { type: 'wran' }); }, focusFile(index) { this.focusedFileIndex = index; const file = this.files[index]; this.form = { ...this.form, fileName: file.fileName, author: file.author, charaName: file.charaName, source: file.source, license: file.license }; // 实现双击复制文件名 if (this.doubleClickTimeoutKey === 0) { this.doubleClickTimeoutKey = setTimeout(() => { this.doubleClickTimeoutKey = 0; }, 300); } else { mw.notify('已复制' + wgULS("文件", "檔案") + '名'); this.copyFileName(this.form.prefix + file.fileName); clearTimeout(this.doubleClickTimeoutKey); this.doubleClickTimeoutKey = 0; } }, addSourceUrlFile() { var url = (prompt(wgULS('请输入文件地址', '請輸入檔案位址') + ':') || '').trim(); if (!url) { return; } this.files.push(this.createFileItem(url)); }, copyFileName(fileName) { const inputTag = document.createElement('input'); inputTag.value = fileName; inputTag.style.cssText = ` position: fixed; left: -9999px; `; document.body.appendChild(inputTag); inputTag.focus(); document.execCommand('selectAll'); document.execCommand('copy'); setTimeout(() => document.body.removeChild(inputTag), 1000); }, asyncCurrentFileInfo() { if (!confirm(wgULS('确定要将当前选中的文件信息(不含文件名)同步到所有文件中?', '確定要將當前選中的檔案資訊(不含檔案名)同步到所有檔案中?'))) { return; } const currentFile = this.files[this.focusedFileIndex]; if (!currentFile) return mw.notify(wgULS('当前未选中文件', '當前未選中檔案')); this.files.forEach(item => { item.author = currentFile.author; item.charaName = currentFile.charaName; item.source = currentFile.source; item.license = currentFile.license; }); mw.notify('已同步'); }, showManual() { alert(wgULS([ '使用说明', '1. 该插件支持拖拽上传、批量上传。', '2. 若文件上传时发生异常,请以监视列表为准。', '3. 每个文件拥有独立的信息,但“分类”和“添加前缀”是共享的。在需要同步每个文件的角色名等信息时可以使用“同步文件信息”的功能。', '4. 什么是“差分上传”:在发生文件名已存在的情况时,自动滤掉已存在的文件。通常用于在上一次批量上传中一部分失败后,再次尝试将之前没传上去的文件重新上传。', '5. 双击文件可以自动复制“前缀 + 文件名”。' ].join('\n'), [ '使用說明', '1. 該外掛程式支援拖拽上傳、批次上傳。', '2. 若檔案上傳時發生異常,請以監視清單為準。', '3. 每個檔案擁有獨立的資訊,但「分類」和「添加前綴」是共享的。在需要同步每個檔案的角色名等資訊時可以使用「同步檔案資訊」的功能。', '4. 什麼是「差分上傳」:在發生檔案名已存在的情況時,自動濾掉已存在的檔案。通常用於在上一次批次上傳中一部分失敗後,再次嘗試將之前沒傳上去的檔案重新上傳。', '5. 雙擊檔案可以自動複製「前綴 + 檔案名」。' ].join('\n'))); }, async submit(diffMode) { if (this.files.length === 0) return mw.notify(wgULS('您还没有上传任何文件', '您還沒有上傳任何檔案'), { type: 'warn' }); if (this.files.some(item => item.fileName === '')) return mw.notify(wgULS('存在文件名为空的文件', '存在檔案名為空的檔案'), { type: 'warn' }); const duplicateFiles = this.files.reduce((result, item) => { const isDuplicate = this.files.filter(item2 => item2.fileName === item.fileName).length > 1; isDuplicate && result.push(item); return result; }, []); if (duplicateFiles.length > 0) return alert([ wgULS('这些文件名发生了重复,请不要给要上传的文件设置相同的名称:', '這些檔案名發生了重複,請不要給要上傳的檔案設定相同的名稱:'), ...duplicateFiles.map(item => item.fileName) ].join('\n')); const authorizedFiles = this.files.filter(item => item.license === 'none:gotoCommons'); if (authorizedFiles.length > 0) return alert([ wgULS('这些文件的授权协议不允许使用上传工具,请在本次上传中删除,并前往特殊页面填写授权信息后上传:', '這些檔案的授權協定不允許使用上傳工具,請在本次上傳中刪除,並前往特殊頁面填寫授權資訊後上傳:'), ...authorizedFiles.map(item => item.fileName), ].join('\n')); if (!confirm(wgULS('确定要开始上传吗?', '確定要開始上傳嗎?'))) { return; } let postData = this.files.map(item => { const metaCategories = (item.charaName ? `[[Category:${item.charaName}${wgULS('图片', '圖片')}]]` : ''); const source = item.source ? `源地址:${item.source}` : ''; const comment = metaCategories + source; const pageContent = [ '== 摘要 ==', metaCategories + this.form.categories.map(item => `[[Category:${item}]]`).join(''), source, '== 许可协议 ==', `{{${item.license}}}` ].join('\n'); return { body: item.body, fileName: this.form.prefix + item.fileName, comment, pageContent }; }); mw.notify(wgULS("开始", "開始") + `${diffMode ? '差分' : ''}` + wgULS("上传", "上傳") + `,共${postData.length}` + wgULS("个文件", "個檔案") + `...`); console.log(`---- FileUploader 开始${diffMode ? '差分' : ''}上传,共${postData.length}个文件 ----`); this.status = 2; const printLogFn = (type = 'info') => msg => { mw.notify(msg, { type }); console.log(msg) }; const printLog = printLogFn(); printLog.warn = printLogFn('warn'); printLog.error = printLogFn('error'); try { const checkedResult = await checkFileNames(postData.map(item => item.fileName)); const existedFiles = postData.filter(item => checkedResult[item.fileName.replace(/^./, s => s.toUpperCase())]); // 首字母转大写,因为checkedResult返回的文件名首字母是大写 if (existedFiles.length > 0 && !diffMode) { alert([ wgULS('这些文件名已被使用,请为对应的文件更换其他名称:', '這些檔案名已被使用,請為對應的檔案更換其他名稱'), ...existedFiles.map(item => item.fileName) ].join('\n')); this.status = 1; return; } if (diffMode) postData = postData.filter(item => !checkedResult[item.fileName.replace(/^./, s => s.toUpperCase())]); if (diffMode && postData.length === 0) { alert(wgULS('差分模式下没有可以上传的文件', '差分模式下沒有可以上傳的檔案')); this.status = 1; return; } printLog.warn("差分" + wgULS("上传共需要上传", "上傳共需要上傳") + `${postData.length}` + wgULS("个文件", "個檔案")); let uploadResults = []; if (postData.length <= 3) { uploadResults = await Promise.all(postData.map(item => new Promise(resolve => { upload(item) .then(() => { printLog(`【${item.fileName}】` + wgULS("上传成功", "上傳成功")); resolve({ fileName: item.fileName, result: true }); }) .catch(() => { printLog.error(`【${item.fileName}】` + wgULS("上传失败", "上傳失敗")); resolve({ fileName: item.fileName, result: false }); }); })) ); } else { alert(wgULS('上传的文件超过三个,执行分段上传,请耐心等待。进入控制台可查看全部日志(按F12后选择Console)。', '上傳的檔案超過三個,執行分段上傳,請耐心等待。進入控制臺可檢視全部日誌(按F12後選擇Console)。')); printLog.warn(wgULS('上传文件超过3个,执行分段上传', '上傳檔案超過3個,執行分段上傳')); // 分段上传 const segmentedPostData = postData.reduce((result, item) => { if (result.length === 0) result.push([]); if (result[result.length - 1].length === 3) result.push([]); result[result.length - 1].push(item); return result; }, []); console.log(segmentedPostData); for (let i=0, len=segmentedPostData.length; i < len; i++) { printLog(`共${len}个分段,现在开始第${i + 1}个`); const segment = segmentedPostData[i]; const segmentedUploadResult = await Promise.all(segment.map(item => new Promise(resolve => { upload(item) .then(() => { printLog(`【${item.fileName}】上传成功`); resolve({ fileName: item.fileName, result: true }); }) .catch(() => { printLog.error(`【${item.fileName}】上传失败`); resolve({ fileName: item.fileName, result: false }); }); })) ); uploadResults.push(...segmentedUploadResult); printLog(`第${i + 1}个分段完成,其中${segmentedUploadResult.filter(item => item.result).length}个成功,${segmentedUploadResult.filter(item => !item.result).length}个失败`); } } const report = wgULS([ `全部上传结果:共计${uploadResults.length}个文件,其中${uploadResults.filter(item => item.result).length}个成功,${uploadResults.filter(item => !item.result).length}个失败`, ...uploadResults.map((item, index) => `${index + 1}. 【${item.fileName}】${item.result ? '成功' : '失败'}`) ].join('\n'), [ `全部上傳結果:共計${uploadResults.length}個檔案,其中${uploadResults.filter(item => item.result).length}個成功,${uploadResults.filter(item => !item.result).length}個失敗`, ...uploadResults.map((item, index) => `${index + 1}. 【${item.fileName}】${item.result ? '成功' : '失敗'}`) ].join('\n')); console.log(report); alert(report); this.status = 3; } catch (e) { console.log('上传流程出现错误', e); mw.notify(wgULS('网络错误,请重试', '網路錯誤,請重試'), { type: 'error' }); this.status = 0; }; } } }); }); //</nowiki>