/** * AKSRT Mini Upload — 小型文件上传器 * * 提供拖拽上传、进度显示、预览功能。 * 用于前台投稿、商品编辑等需要文件上传的页面。 * * @package AKSRT */ (function () { 'use strict'; /** * 初始化小型上传器 * * @param {HTMLElement} container 上传器容器 * @param {Object} options 配置项 */ function MiniUpload(container, options) { if (!container) return; this.options = Object.assign({ maxFileSize: 5 * 1024 * 1024, // 5MB allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], multiple: false, uploadUrl: '', fieldName: 'file', nonce: '', onSelect: null, onSuccess: null, onError: null, }, options || {}); this.container = container; this.files = []; this.init(); } MiniUpload.prototype.init = function () { var self = this; // 创建文件输入 this.input = document.createElement('input'); this.input.type = 'file'; this.input.accept = this.options.allowedTypes.join(','); this.input.multiple = this.options.multiple; this.input.style.display = 'none'; this.container.appendChild(this.input); // 创建拖拽区域 this.dropzone = document.createElement('div'); this.dropzone.className = 'aksrt-mini-upload__dropzone'; this.dropzone.innerHTML = '
' + '
点击或拖拽文件到此处上传
' + '
支持 ' + this.options.allowedTypes.map(function (t) { return t.split('/')[1].toUpperCase(); }).join(', ') + ',最大 ' + Math.round(this.options.maxFileSize / 1024 / 1024) + 'MB
'; this.container.appendChild(this.dropzone); // 创建预览区域 this.preview = document.createElement('div'); this.preview.className = 'aksrt-mini-upload__preview'; this.container.appendChild(this.preview); // 事件绑定 this.dropzone.addEventListener('click', function () { self.input.click(); }); this.dropzone.addEventListener('dragover', function (e) { e.preventDefault(); self.dropzone.classList.add('aksrt-mini-upload__dropzone--dragover'); }); this.dropzone.addEventListener('dragleave', function () { self.dropzone.classList.remove('aksrt-mini-upload__dropzone--dragover'); }); this.dropzone.addEventListener('drop', function (e) { e.preventDefault(); self.dropzone.classList.remove('aksrt-mini-upload__dropzone--dragover'); if (e.dataTransfer.files.length > 0) { self.handleFiles(e.dataTransfer.files); } }); this.input.addEventListener('change', function () { if (self.input.files.length > 0) { self.handleFiles(self.input.files); } }); }; MiniUpload.prototype.handleFiles = function (fileList) { var self = this; var files = Array.prototype.slice.call(fileList); files.forEach(function (file) { // 验证文件类型 if (self.options.allowedTypes.length > 0 && self.options.allowedTypes.indexOf(file.type) === -1) { self.showError('不支持的文件类型: ' + file.type); return; } // 验证文件大小 if (file.size > self.options.maxFileSize) { self.showError('文件大小超过限制: ' + Math.round(file.size / 1024 / 1024) + 'MB'); return; } self.files.push(file); self.addPreview(file); if (typeof self.options.onSelect === 'function') { self.options.onSelect(file); } // 如果有上传 URL,自动上传 if (self.options.uploadUrl) { self.uploadFile(file); } }); }; MiniUpload.prototype.addPreview = function (file) { var self = this; var item = document.createElement('div'); item.className = 'aksrt-mini-upload__item'; if (file.type.startsWith('image/')) { var reader = new FileReader(); reader.onload = function (e) { item.innerHTML = '' + '
' + '' + file.name + '' + '' + self.formatSize(file.size) + '' + '
' + '' + '
'; self.bindRemove(item, file); }; reader.readAsDataURL(file); } else { item.innerHTML = '
' + '
' + '' + file.name + '' + '' + self.formatSize(file.size) + '' + '
' + '' + '
'; this.bindRemove(item, file); } this.preview.appendChild(item); }; MiniUpload.prototype.bindRemove = function (item, file) { var self = this; var removeBtn = item.querySelector('.aksrt-mini-upload__remove'); if (removeBtn) { removeBtn.addEventListener('click', function () { var index = self.files.indexOf(file); if (index > -1) self.files.splice(index, 1); item.remove(); }); } }; MiniUpload.prototype.uploadFile = function (file) { var self = this; var formData = new FormData(); formData.append(this.options.fieldName, file); if (this.options.nonce) { formData.append('_wpnonce', this.options.nonce); } var xhr = new XMLHttpRequest(); xhr.open('POST', this.options.uploadUrl, true); // 进度更新 var item = this.preview.lastElementChild; var progressBar = item ? item.querySelector('.aksrt-mini-upload__progress-bar') : null; xhr.upload.onprogress = function (e) { if (e.lengthComputable && progressBar) { var percent = Math.round((e.loaded / e.total) * 100); progressBar.style.width = percent + '%'; } }; xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { try { var response = JSON.parse(xhr.responseText); if (typeof self.options.onSuccess === 'function') { self.options.onSuccess(response, file); } if (item) item.classList.add('aksrt-mini-upload__item--success'); } catch (e) { self.showError('上传响应解析失败'); } } else { self.showError('上传失败: HTTP ' + xhr.status); } }; xhr.onerror = function () { self.showError('网络错误,上传失败'); if (item) item.classList.add('aksrt-mini-upload__item--error'); }; xhr.send(formData); }; MiniUpload.prototype.showError = function (msg) { if (typeof this.options.onError === 'function') { this.options.onError(msg); } console.error('[AKSRT MiniUpload]', msg); }; MiniUpload.prototype.formatSize = function (bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; }; MiniUpload.prototype.getFiles = function () { return this.files; }; MiniUpload.prototype.clear = function () { this.files = []; this.preview.innerHTML = ''; this.input.value = ''; }; /** * 分片上传大文件 * @param {File} file - 要上传的文件 * @param {Object} opts - { chunkSize, uploadUrl, fieldName, nonce, onProgress, onComplete, onError } */ MiniUpload.prototype.uploadChunked = function (file, opts) { var chunkSize = (opts.chunkSize || 2) * 1024 * 1024; // default 2MB var totalChunks = Math.ceil(file.size / chunkSize); var currentChunk = 0; var fileId = file.name + '-' + file.size + '-' + file.lastModified; function uploadNext() { if (currentChunk >= totalChunks) { if (opts.onComplete) opts.onComplete({ fileId: fileId, totalChunks: totalChunks }); return; } var start = currentChunk * chunkSize; var end = Math.min(start + chunkSize, file.size); var blob = file.slice(start, end); var formData = new FormData(); formData.append(opts.fieldName || 'file', blob, file.name); formData.append('chunk', currentChunk); formData.append('total_chunks', totalChunks); formData.append('file_id', fileId); formData.append('file_name', file.name); formData.append('file_size', file.size); if (opts.nonce) formData.append('_ajax_nonce', opts.nonce); var xhr = new XMLHttpRequest(); xhr.open('POST', opts.uploadUrl, true); xhr.upload.onprogress = function (e) { if (e.lengthComputable && opts.onProgress) { var chunkProgress = e.loaded / e.total; var totalProgress = (currentChunk + chunkProgress) / totalChunks; opts.onProgress(totalProgress, currentChunk, totalChunks); } }; xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { currentChunk++; uploadNext(); } else { if (opts.onError) opts.onError('上传失败: HTTP ' + xhr.status); } }; xhr.onerror = function () { if (opts.onError) opts.onError('网络错误'); }; xhr.send(formData); } uploadNext(); }; /** * 快速文件标识(替代 spark-md5,用文件名+大小+最后修改时间生成简易 hash) */ MiniUpload.prototype.getFileId = function (file) { return file.name + '-' + file.size + '-' + file.lastModified; }; // 自动初始化 function autoInit() { document.querySelectorAll('[data-aksrt-mini-upload]').forEach(function (el) { if (el.dataset.miniUploadInit) return; el.dataset.miniUploadInit = '1'; var options = {}; try { options = JSON.parse(el.dataset.aksrtMiniUpload || '{}'); } catch (e) {} new MiniUpload(el, options); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', autoInit); } else { autoInit(); } // 暴露到全局 window.AksrtMiniUpload = MiniUpload; })();