/**
* 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(root) {
var containers = [];
if (root && root.nodeType === 1 && root.matches('[data-aksrt-mini-upload]')) containers.push(root);
Array.prototype.push.apply(containers, (root || document).querySelectorAll('[data-aksrt-mini-upload]'));
containers.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);
});
}
window.AksrtFrontend.register('aksrt-mini-upload', autoInit);
// 暴露到全局
window.AksrtMiniUpload = MiniUpload;
})();