- rehydrate.js 新增 boot()/aksrt:ready 事件,支持 boot 后晚注册立即以 init 触发 - aksrt-runtime.js 初始加载改走 AksrtFrontend.boot,tooltip/倒计时注册为组件 - 约 30 个前端脚本(首页、评论、支付、路由、页面叶子、widgets) 从直接 DOMContentLoaded 迁移为 AksrtFrontend.register,并加幂等标记 - section-navs 依赖改为 aksrt-runtime,保证注册时序 - 清理死代码:删除孤儿 loader.js/main.js 及 runtime/page-navs 遗留副本 - loader.js 移除已迁移模块的 tbquire 路径映射,main.js 相应瘦身 - build-public-assets.mjs 增加 runtime/main.min.js 自动压缩步骤 - 新增 4 个契约测试(homepage-dom/rehydrate-runtime/global-dom-init/ public-bundle-parity),3 个既有 harness 适配注册表初始化方式
332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
/**
|
|
* 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 =
|
|
'<div class="aksrt-mini-upload__icon"><i class="fa fas fa-cloud-upload-alt"></i></div>' +
|
|
'<div class="aksrt-mini-upload__text">点击或拖拽文件到此处上传</div>' +
|
|
'<div class="aksrt-mini-upload__hint">支持 ' + this.options.allowedTypes.map(function (t) {
|
|
return t.split('/')[1].toUpperCase();
|
|
}).join(', ') + ',最大 ' + Math.round(this.options.maxFileSize / 1024 / 1024) + 'MB</div>';
|
|
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 =
|
|
'<img src="' + e.target.result + '" class="aksrt-mini-upload__thumb">' +
|
|
'<div class="aksrt-mini-upload__info">' +
|
|
'<span class="aksrt-mini-upload__name">' + file.name + '</span>' +
|
|
'<span class="aksrt-mini-upload__size">' + self.formatSize(file.size) + '</span>' +
|
|
'</div>' +
|
|
'<button type="button" class="aksrt-mini-upload__remove"><i class="fa fas fa-times"></i></button>' +
|
|
'<div class="aksrt-mini-upload__progress"><div class="aksrt-mini-upload__progress-bar"></div></div>';
|
|
self.bindRemove(item, file);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
} else {
|
|
item.innerHTML =
|
|
'<div class="aksrt-mini-upload__file-icon"><i class="fa fas fa-file"></i></div>' +
|
|
'<div class="aksrt-mini-upload__info">' +
|
|
'<span class="aksrt-mini-upload__name">' + file.name + '</span>' +
|
|
'<span class="aksrt-mini-upload__size">' + self.formatSize(file.size) + '</span>' +
|
|
'</div>' +
|
|
'<button type="button" class="aksrt-mini-upload__remove"><i class="fa fas fa-times"></i></button>' +
|
|
'<div class="aksrt-mini-upload__progress"><div class="aksrt-mini-upload__progress-bar"></div></div>';
|
|
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;
|
|
})();
|