微件:BatchUpload:修订间差异
来自植物大战僵尸杂交版Wiki
更多操作
无编辑摘要 |
无编辑摘要 |
||
| 第47行: | 第47行: | ||
} | } | ||
.batch-upload-item .file-rename:focus { border-color: #4CAF50; outline: none; } | .batch-upload-item .file-rename:focus { border-color: #4CAF50; outline: none; } | ||
.batch-upload-item .file-status { font-size: 12px; min-width: | .batch-upload-item .file-status { font-size: 12px; min-width: 70px; text-align: right; } | ||
.status-waiting { color: #999; } | .status-waiting { color: #999; } | ||
.status-uploading { color: #2196F3; } | .status-uploading { color: #2196F3; } | ||
.status-success { color: #4CAF50; } | .status-success { color: #4CAF50; } | ||
.status-error { color: #f44336; } | .status-error { color: #f44336; } | ||
.status-conflict { color: #FF9800; } | |||
.status-skipped { color: #9E9E9E; } | |||
.batch-upload-progress { | .batch-upload-progress { | ||
width: 100%; height: 6px; background: #e0e0e0; | width: 100%; height: 6px; background: #e0e0e0; | ||
| 第65行: | 第67行: | ||
.batch-upload-remove:hover { color: #d32f2f; } | .batch-upload-remove:hover { color: #d32f2f; } | ||
.batch-upload-ext { font-size: 12px; color: #999; margin-left: 2px; } | .batch-upload-ext { font-size: 12px; color: #999; margin-left: 2px; } | ||
.conflict-actions { display: inline-flex; gap: 4px; margin-left: 8px; } | |||
.conflict-btn { | |||
padding: 2px 8px; font-size: 11px; border: 1px solid #ccc; | |||
border-radius: 3px; cursor: pointer; background: #f9f9f9; | |||
} | |||
.conflict-btn.overwrite { border-color: #4CAF50; color: #4CAF50; } | |||
.conflict-btn.overwrite:hover { background: #4CAF50; color: #fff; } | |||
.conflict-btn.skip { border-color: #f44336; color: #f44336; } | |||
.conflict-btn.skip:hover { background: #f44336; color: #fff; } | |||
.conflict-notice { | |||
background: #fff3e0; border: 1px solid #FF9800; border-radius: 8px; | |||
padding: 10px 15px; margin: 10px 0; display: none; text-align: left; font-size: 14px; | |||
} | |||
.conflict-notice button { | |||
padding: 6px 15px; margin: 5px 5px 0 0; border: none; | |||
border-radius: 5px; cursor: pointer; font-size: 13px; color: #fff; | |||
} | |||
.conflict-notice .btn-overwrite-all { background: #4CAF50; } | |||
.conflict-notice .btn-skip-all { background: #f44336; } | |||
</style> | </style> | ||
| 第73行: | 第94行: | ||
<button class="batch-upload-btn" id="batch-upload-select">选择图片</button> | <button class="batch-upload-btn" id="batch-upload-select">选择图片</button> | ||
<button class="batch-upload-btn" id="batch-upload-clear">清空列表</button> | <button class="batch-upload-btn" id="batch-upload-clear">清空列表</button> | ||
</div> | |||
<div class="conflict-notice" id="conflict-notice"> | |||
<strong>⚠️ 检测到重名文件</strong>,请为每个文件选择"覆盖"或"跳过": | |||
<div id="conflict-actions-global"></div> | |||
</div> | </div> | ||
<div class="batch-upload-progress" id="batch-upload-progress"> | <div class="batch-upload-progress" id="batch-upload-progress"> | ||
| 第79行: | 第104行: | ||
<div class="batch-upload-list" id="batch-upload-list"></div> | <div class="batch-upload-list" id="batch-upload-list"></div> | ||
<div class="batch-upload-summary" id="batch-upload-summary"></div> | <div class="batch-upload-summary" id="batch-upload-summary"></div> | ||
<button class="batch-upload-btn" id="batch-upload-start" disabled> | <button class="batch-upload-btn" id="batch-upload-start" disabled>检测并上传</button> | ||
</div> | </div> | ||
| 第92行: | 第117行: | ||
var $progressBar = document.getElementById('batch-upload-progress-bar'); | var $progressBar = document.getElementById('batch-upload-progress-bar'); | ||
var $summary = document.getElementById('batch-upload-summary'); | var $summary = document.getElementById('batch-upload-summary'); | ||
var $conflictNotice = document.getElementById('conflict-notice'); | |||
var $conflictGlobal = document.getElementById('conflict-actions-global'); | |||
var files = []; | var files = []; | ||
| 第97行: | 第124行: | ||
var totalCount = 0; | var totalCount = 0; | ||
var isUploading = false; | var isUploading = false; | ||
var conflictResolved = false; | |||
function getExt(filename) { | function getExt(filename) { | ||
| 第108行: | 第136行: | ||
} | } | ||
var fileInput = document.createElement('input'); | var fileInput = document.createElement('input'); | ||
fileInput.type = 'file'; | fileInput.type = 'file'; | ||
| 第125行: | 第152行: | ||
}); | }); | ||
$container.addEventListener('dragover', function(e) { | $container.addEventListener('dragover', function(e) { | ||
e.preventDefault(); | e.preventDefault(); | ||
| 第141行: | 第167行: | ||
$clearBtn.addEventListener('click', function() { | $clearBtn.addEventListener('click', function() { | ||
files = []; | files = []; | ||
conflictResolved = false; | |||
$conflictNotice.style.display = 'none'; | |||
renderList(); | renderList(); | ||
updateStartButton(); | updateStartButton(); | ||
| 第152行: | 第180行: | ||
file._targetName = getNameWithoutExt(file.name); | file._targetName = getNameWithoutExt(file.name); | ||
file._ext = getExt(file.name); | file._ext = getExt(file.name); | ||
file._conflictAction = 'ask'; // ask | overwrite | skip | |||
files.push(file); | files.push(file); | ||
} | } | ||
} | } | ||
conflictResolved = false; | |||
$conflictNotice.style.display = 'none'; | |||
renderList(); | renderList(); | ||
updateStartButton(); | updateStartButton(); | ||
| 第169行: | 第200行: | ||
var item = document.createElement('div'); | var item = document.createElement('div'); | ||
item.className = 'batch-upload-item'; | item.className = 'batch-upload-item'; | ||
item.setAttribute('data-index', index); | |||
var actionHtml = ''; | |||
if (file._conflictAction === 'overwrite') { | |||
actionHtml = '<span class="conflict-actions"><span style="color:#4CAF50;font-size:12px;">覆盖</span></span>'; | |||
} else if (file._conflictAction === 'skip') { | |||
actionHtml = '<span class="conflict-actions"><span style="color:#f44336;font-size:12px;">跳过</span></span>'; | |||
} | |||
item.innerHTML = | item.innerHTML = | ||
'<div class="file-info">' + | '<div class="file-info">' + | ||
| 第175行: | 第215行: | ||
'<div class="file-original">原始: ' + file.name + ' (' + sizeStr + ')</div>' + | '<div class="file-original">原始: ' + file.name + ' (' + sizeStr + ')</div>' + | ||
'</div>' + | '</div>' + | ||
'<span class="file-status status-waiting" data-index="' + index + '"> | actionHtml + | ||
'<span class="file-status status-waiting" data-index="' + index + '">等待检测</span>' + | |||
'<span class="batch-upload-remove" data-index="' + index + '" title="移除">×</span>'; | '<span class="batch-upload-remove" data-index="' + index + '" title="移除">×</span>'; | ||
$list.appendChild(item); | $list.appendChild(item); | ||
}); | }); | ||
$list.querySelectorAll('.file-rename').forEach(function(input) { | $list.querySelectorAll('.file-rename').forEach(function(input) { | ||
input.addEventListener('input', function() { | input.addEventListener('input', function() { | ||
var idx = parseInt(this.getAttribute('data-index')); | var idx = parseInt(this.getAttribute('data-index')); | ||
if (files[idx]) files[idx]._targetName = this.value.trim(); | if (files[idx]) { | ||
files[idx]._targetName = this.value.trim(); | |||
files[idx]._conflictAction = 'ask'; | |||
conflictResolved = false; | |||
$conflictNotice.style.display = 'none'; | |||
} | |||
}); | }); | ||
}); | }); | ||
$list.querySelectorAll('.batch-upload-remove').forEach(function(btn) { | $list.querySelectorAll('.batch-upload-remove').forEach(function(btn) { | ||
btn.addEventListener('click', function() { | btn.addEventListener('click', function() { | ||
| 第201行: | 第245行: | ||
function updateStartButton() { | function updateStartButton() { | ||
$startBtn.disabled = files.length === 0 || isUploading; | $startBtn.disabled = files.length === 0 || isUploading; | ||
if (conflictResolved) { | |||
$startBtn.textContent = '开始上传'; | |||
} else { | |||
$startBtn.textContent = '检测并上传'; | |||
} | |||
} | } | ||
function getEditToken() { | function getEditToken() { | ||
return mw.user.tokens.get('csrfToken'); | return mw.user.tokens.get('csrfToken'); | ||
} | |||
// 检查文件是否存在 | |||
function checkFileExists(filename) { | |||
return new Promise(function(resolve) { | |||
var xhr = new XMLHttpRequest(); | |||
xhr.open('GET', mw.util.wikiScript('api') + '?action=query&titles=File:' + encodeURIComponent(filename) + '&format=json&origin=*', true); | |||
xhr.onload = function() { | |||
try { | |||
var data = JSON.parse(xhr.responseText); | |||
var pages = data.query.pages; | |||
for (var id in pages) { | |||
if (parseInt(id) > 0) { resolve(true); return; } | |||
} | |||
resolve(false); | |||
} catch(e) { resolve(false); } | |||
}; | |||
xhr.onerror = function() { resolve(false); }; | |||
xhr.send(); | |||
}); | |||
} | } | ||
| 第242行: | 第311行: | ||
} | } | ||
// 显示冲突处理 UI | |||
function showConflictUI(conflicts) { | |||
$conflictNotice.style.display = 'block'; | |||
$conflictGlobal.innerHTML = | |||
'<button class="btn-overwrite-all">全部覆盖</button>' + | |||
'<button class="btn-skip-all">全部跳过</button>'; | |||
$conflictGlobal.querySelector('.btn-overwrite-all').addEventListener('click', function() { | |||
conflicts.forEach(function(idx) { files[idx]._conflictAction = 'overwrite'; }); | |||
conflictResolved = true; | |||
$conflictNotice.style.display = 'none'; | |||
renderList(); | |||
updateStartButton(); | |||
}); | |||
$conflictGlobal.querySelector('.btn-skip-all').addEventListener('click', function() { | |||
conflicts.forEach(function(idx) { files[idx]._conflictAction = 'skip'; }); | |||
conflictResolved = true; | |||
$conflictNotice.style.display = 'none'; | |||
renderList(); | |||
updateStartButton(); | |||
}); | |||
// 在列表中为每个冲突项添加覆盖/跳过按钮 | |||
conflicts.forEach(function(idx) { | |||
var item = $list.querySelector('.batch-upload-item[data-index="' + idx + '"]'); | |||
if (!item) return; | |||
var statusEl = item.querySelector('.file-status'); | |||
if (statusEl) { | |||
statusEl.classList.remove('status-waiting'); | |||
statusEl.classList.add('status-conflict'); | |||
statusEl.textContent = '⚠️ 重名'; | |||
} | |||
// 移除旧的动作区 | |||
var oldActions = item.querySelector('.conflict-actions'); | |||
if (oldActions) oldActions.remove(); | |||
var actions = document.createElement('span'); | |||
actions.className = 'conflict-actions'; | |||
actions.innerHTML = | |||
'<button class="conflict-btn overwrite" data-idx="' + idx + '">覆盖</button>' + | |||
'<button class="conflict-btn skip" data-idx="' + idx + '">跳过</button>'; | |||
item.appendChild(actions); | |||
}); | |||
// 绑定单条操作 | |||
$list.querySelectorAll('.conflict-btn.overwrite').forEach(function(btn) { | |||
btn.addEventListener('click', function(e) { | |||
e.stopPropagation(); | |||
var idx = parseInt(this.getAttribute('data-idx')); | |||
files[idx]._conflictAction = 'overwrite'; | |||
var item = $list.querySelector('.batch-upload-item[data-index="' + idx + '"]'); | |||
var statusEl = item.querySelector('.file-status'); | |||
statusEl.classList.remove('status-conflict'); | |||
statusEl.classList.add('status-waiting'); | |||
statusEl.textContent = '等待上传'; | |||
var actionsEl = item.querySelector('.conflict-actions'); | |||
if (actionsEl) actionsEl.innerHTML = '<span style="color:#4CAF50;font-size:12px;">覆盖</span>'; | |||
checkAllConflictsResolved(); | |||
}); | |||
}); | |||
$list.querySelectorAll('.conflict-btn.skip').forEach(function(btn) { | |||
btn.addEventListener('click', function(e) { | |||
e.stopPropagation(); | |||
var idx = parseInt(this.getAttribute('data-idx')); | |||
files[idx]._conflictAction = 'skip'; | |||
var item = $list.querySelector('.batch-upload-item[data-index="' + idx + '"]'); | |||
var statusEl = item.querySelector('.file-status'); | |||
statusEl.classList.remove('status-conflict'); | |||
statusEl.classList.add('status-skipped'); | |||
statusEl.textContent = '⏭️ 跳过'; | |||
var actionsEl = item.querySelector('.conflict-actions'); | |||
if (actionsEl) actionsEl.innerHTML = '<span style="color:#f44336;font-size:12px;">跳过</span>'; | |||
checkAllConflictsResolved(); | |||
}); | |||
}); | |||
} | |||
function checkAllConflictsResolved() { | |||
var allResolved = true; | |||
files.forEach(function(f) { | |||
if (f._conflictAction === 'ask') allResolved = false; | |||
}); | |||
if (allResolved) { | |||
conflictResolved = true; | |||
$conflictNotice.style.display = 'none'; | |||
updateStartButton(); | |||
} | |||
} | |||
// 主流程:先检测再上传 | |||
$startBtn.addEventListener('click', function() { | $startBtn.addEventListener('click', function() { | ||
if (isUploading || files.length === 0) return; | |||
// 如果冲突已解决,直接上传 | |||
if (conflictResolved) { | |||
startUpload(); | |||
return; | |||
} | |||
// 先检测重名 | |||
isUploading = true; | |||
updateStartButton(); | |||
$selectBtn.disabled = true; | |||
$clearBtn.disabled = true; | |||
$startBtn.textContent = '检测中...'; | |||
$list.querySelectorAll('.file-rename').forEach(function(i) { i.disabled = true; }); | |||
$list.querySelectorAll('.batch-upload-remove').forEach(function(i) { i.style.display = 'none'; }); | |||
var checkQueue = Promise.resolve(); | |||
var conflicts = []; | |||
files.forEach(function(file, index) { | |||
checkQueue = checkQueue.then(function() { | |||
var targetName = (file._targetName || getNameWithoutExt(file.name)) + file._ext; | |||
return checkFileExists(targetName).then(function(exists) { | |||
if (exists) { | |||
conflicts.push(index); | |||
} else { | |||
var statusEl = $list.querySelector('.file-status[data-index="' + index + '"]'); | |||
if (statusEl) { | |||
statusEl.classList.remove('status-waiting'); | |||
statusEl.classList.add('status-waiting'); | |||
statusEl.textContent = '✅ 新文件'; | |||
} | |||
} | |||
}); | |||
}); | |||
}); | |||
checkQueue.then(function() { | |||
isUploading = false; | |||
$selectBtn.disabled = false; | |||
$clearBtn.disabled = false; | |||
updateStartButton(); | |||
if (conflicts.length > 0) { | |||
showConflictUI(conflicts); | |||
} else { | |||
conflictResolved = true; | |||
updateStartButton(); | |||
startUpload(); | |||
} | |||
}); | |||
}); | |||
function startUpload() { | |||
if (isUploading || files.length === 0) return; | if (isUploading || files.length === 0) return; | ||
isUploading = true; | isUploading = true; | ||
uploadedCount = 0; | uploadedCount = 0; | ||
totalCount = | totalCount = 0; | ||
updateStartButton(); | updateStartButton(); | ||
$selectBtn.disabled = true; | $selectBtn.disabled = true; | ||
| 第255行: | 第469行: | ||
$list.querySelectorAll('.file-rename').forEach(function(i) { i.disabled = true; }); | $list.querySelectorAll('.file-rename').forEach(function(i) { i.disabled = true; }); | ||
$list.querySelectorAll('.batch-upload-remove').forEach(function(i) { i.style.display = 'none'; }); | $list.querySelectorAll('.batch-upload-remove').forEach(function(i) { i.style.display = 'none'; }); | ||
$list.querySelectorAll('.conflict-actions').forEach(function(i) { i.style.display = 'none'; }); | |||
// 过滤掉跳过的文件 | |||
var toUpload = []; | |||
files.forEach(function(f, idx) { | |||
if (f._conflictAction !== 'skip') { | |||
toUpload.push({ file: f, index: idx }); | |||
} | |||
}); | |||
totalCount = toUpload.length; | |||
if (totalCount === 0) { | |||
isUploading = false; | |||
$selectBtn.disabled = false; | |||
$clearBtn.disabled = false; | |||
updateStartButton(); | |||
$summary.style.display = 'block'; | |||
$summary.innerHTML = '所有文件已跳过,没有上传。'; | |||
$progress.style.display = 'none'; | |||
files = []; | |||
return; | |||
} | |||
var uploadQueue = Promise.resolve(); | var uploadQueue = Promise.resolve(); | ||
toUpload.forEach(function(item) { | |||
uploadQueue = uploadQueue.then(function() { | uploadQueue = uploadQueue.then(function() { | ||
var statusEl = $list.querySelector('.file-status[data-index="' + index + '"]'); | var statusEl = $list.querySelector('.file-status[data-index="' + item.index + '"]'); | ||
if (statusEl) { | if (statusEl) { | ||
statusEl.classList.remove('status-waiting'); | statusEl.classList.remove('status-waiting', 'status-conflict', 'status-skipped'); | ||
statusEl.classList.add('status-uploading'); | statusEl.classList.add('status-uploading'); | ||
statusEl.textContent = '上传中...'; | statusEl.textContent = '上传中...'; | ||
} | } | ||
return uploadFile(file, index).then(function(result) { | return uploadFile(item.file, item.index).then(function(result) { | ||
uploadedCount++; | uploadedCount++; | ||
var progress = Math.round((uploadedCount / totalCount) * 100); | var progress = Math.round((uploadedCount / totalCount) * 100); | ||
| 第290行: | 第526行: | ||
var successCount = $list.querySelectorAll('.status-success').length; | var successCount = $list.querySelectorAll('.status-success').length; | ||
var failCount = $list.querySelectorAll('.status-error').length; | var failCount = $list.querySelectorAll('.status-error').length; | ||
var skipCount = $list.querySelectorAll('.status-skipped').length; | |||
var msg = '上传完成!成功 ' + successCount + ' 张'; | |||
if (failCount > 0) msg += ',失败 ' + failCount + ' 张'; | |||
if (skipCount > 0) msg += ',跳过 ' + skipCount + ' 张'; | |||
msg += '。'; | |||
$summary.style.display = 'block'; | $summary.style.display = 'block'; | ||
$summary.innerHTML = | $summary.innerHTML = msg; | ||
files = []; | files = []; | ||
conflictResolved = false; | |||
setTimeout(function() { | setTimeout(function() { | ||
if (files.length === 0 && !isUploading) { | if (files.length === 0 && !isUploading) { | ||
| 第300行: | 第542行: | ||
}, 5000); | }, 5000); | ||
}); | }); | ||
} | } | ||
})(); | })(); | ||
</script> | </script> | ||
</includeonly> | </includeonly> | ||