(function ($) {
'use strict';
var G3File = {
files: [],
selectedFiles: new Set(),
loadScripts: function (items, callback) {
var self = this;
var remaining = items.length;
function done() {
remaining--;
if (remaining === 0) {
callback && callback();
}
}
items.forEach(function (item) {
// 已加载
if (item.check && item.check()) {
done();
return;
}
// 防重复加载
var key = item.key;
if (window[key + '_loading']) {
var timer = setInterval(function () {
if (item.check && item.check()) {
clearInterval(timer);
done();
}
}, 50);
return;
}
window[key + '_loading'] = true;
var script = document.createElement('script');
script.src = item.src;
script.onload = function () {
window[key + '_loading'] = false;
done();
};
script.onerror = function () {
window[key + '_loading'] = false;
console.error(key + ' load failed');
done();
};
document.head.appendChild(script);
});
},
// =====================
// 初始化
// =====================
init: function (containerId, options) {
var self = this;
this.$container = $('#' + containerId);
this.options = $.extend({}, this.defaults, options);
this.language = this.languages[this.options.language] || this.languages['zh-CN'];
this.files = this.options.data || [];
this.loadScripts([
{
key: 'fileSaver',
src: '/custom-file/libs/FileSaver.min.js',
check: function () {
return typeof window.saveAs === 'function';
}
},
{
key: 'jsZip',
src: '/custom-file/libs/jszip.min.js',
check: function () {
return typeof window.JSZip === 'function';
}
}
], function () {
self.render();
self.bindEvents();
self.updatePermissions(self.options.permissions);
if (self.options.data?.length) {
self.loadFiles(self.options.data);
}
});
return this;
},
// =====================
// 默认配置
// =====================
defaults: {
data: [],
onDelete: null,
onDownload: null,
onSelect: null,
onUpload: null,
language: 'zh-CN',
maxFileSize: 10 * 1024 * 1024, // 默认最大10MB
permissions: {
select: true,
upload: true,
download: true,
delete: true,
view: true
}
},
// =====================
// 多语言
// =====================
languages: {
'zh-CN': {
selectAll: '全选',
download: '批量下载',
delete: '删除',
upload: '上传',
select: '选择',
confirmDelete: '确定要删除选中的 {count} 个文件吗?',
downloading: '准备下载 {count} 个文件',
deleting: '删除中...',
name: '名称',
size: '大小',
date: '日期',
pending: '未上传',
selectFiles: '选择文件',
emptyTip1: '拖拽文件到这里 …',
emptyTip2: '支持多文件同时上传/下载',
emptyTip3: '支持粘贴图片上传',
fileSizeError: '文件大小超过限制'
},
'en-US': {
selectAll: 'Select All',
download: 'Batch Download',
delete: 'Delete',
upload: 'Upload',
select: 'Select',
confirmDelete: 'Are you sure to delete {count} selected files?',
downloading: 'Preparing to download {count} files',
deleting: 'Deleting...',
name: 'Name',
size: 'Size',
date: 'Date',
pending: 'Pending',
selectFiles: 'Select Files',
fileSizeError: 'File size exceeds limit'
}
},
// =====================
// 静态icon合计
// =====================
staticIconMap: {
download: `
`,
delete: `
`,
select: `
`,
upload: `
`,
more: `
`,
pdf: `
`,
video: `
`,
default_file: `
`
},
// =====================
// 生成唯一ID
// =====================
generateUniqueId: function () {
return 'file_' + Math.random().toString(36).slice(2) + Date.now();
},
// =====================
// 获取文件图标
// =====================
getFileIcon: function (filename) {
var ext = filename.split('.').pop().toLowerCase();
return this.staticIconMap[ext] || this.staticIconMap['default_file'];
},
// =====================
// 更新语言
// =====================
updateLanguage: function (language) {
this.language = this.languages[language] || this.languages['zh-CN'];
this.render();
return this;
},
// =====================
// 渲染
// =====================
render: function () {
const html = `
main
${this.language.emptyTip1}
${this.language.emptyTip2}
${this.language.emptyTip3}
`;
this.$container.html(html);
this.$fileList = this.$container.find('.main');
this.$emptyTip = this.$container.find('.empty-tip');
this.$checkAll = this.$container.find('.check-all-checkbox');
this.$btnDownload = this.$container.find('.download-all');
this.$btnDelete = this.$container.find('.delete-all');
this.$btnUpload = this.$container.find('.upload');
this.$btnSelect = this.$container.find('.select');
this.$fileInput = this.$container.find('.file-input');
this.$pendingCount = this.$container.find('.pending-number');
return this;
},
bindEvents: function () {
var self = this;
// 选择
this.$checkAll.off('change').on('change', function () {
var checked = $(this).is(':checked');
self.selectedFiles.clear();
if (checked) {
self.files.forEach(function (file) {
self.selectedFiles.add(file._id);
});
}
self.$fileList.find('.file-checkbox').prop('checked', checked);
self.updateToolbarState();
});
// 批量下载
this.$btnDownload.off('click').on('click', function () {
var idSet = self.selectedFiles || new Set();
var files = self.files.filter(function (item) {
return idSet.has(item._id);
});
if (typeof self.options?.onDownloadBatch === 'function') {
self.options.onDownloadBatch(files);
return;
}
self.internalDownloadBatch(files);
});
// 批量删除
this.$btnDelete.off('click').on('click', function () {
var idSet = self.selectedFiles || new Set();
if (idSet.size === 0) return;
var deleteFiles = self.files.filter(function (item) {
return idSet.has(item._id);
});
// =====================
// 1. 优先走外部回调(如果你要接后端)
// =====================
if (typeof self.options?.onDeleteBatch === 'function') {
self.options.onDeleteBatch(deleteFiles, function (success) {
if (!success) return;
// 成功后再清理本地
self.internalDeleteBatch(deleteFiles);
});
return;
}
// =====================
// 2. 没有外部逻辑 -> 直接本地删除
// =====================
self.internalDeleteBatch(deleteFiles);
});
// =====================
// 选择按钮点击事件(选择本地文件)
// =====================
this.$btnSelect.off('click').on('click', function () {
self.$fileInput.click();
});
// =====================
// 上传按钮点击事件(交给外部处理)
// =====================
this.$btnUpload.off('click').on('click', function () {
// 获取所有未上传的文件
const pendingFiles = self.files.filter(file => !file.uploaded);
if (typeof self.options.onUpload === 'function') {
self.options.onUpload(pendingFiles);
}
});
// =====================
// 文件选择事件
// =====================
this.$fileInput.off('change').on('change', function (e) {
var files = e.target.files;
if (!files || files.length === 0) return;
var maxSize = self.options.maxFileSize || 10 * 1024 * 1024;
var validFiles = [];
var invalidFiles = [];
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (file.size > maxSize) {
invalidFiles.push(file);
} else {
validFiles.push(file);
}
}
if (invalidFiles.length > 0) {
var errorMsg = self.language.fileSizeError || '文件大小超过限制';
alert(errorMsg + ': ' + invalidFiles.map(f => f.name).join(', '));
}
if (validFiles.length > 0) {
// 在本地临时插入文件
var tempFiles = validFiles.map(function (file) {
// 为图片文件创建本地临时连接
let url = '';
if (self.isImageFile(file.name)) {
url = URL.createObjectURL(file);
}
return {
_id: self.generateUniqueId(),
name: file.name,
size: file.size,
uploadDate: new Date().toISOString(),
uploaded: false, // 标记为未上传
url: url, // 图片文件使用本地临时连接,其他文件为空
file: file // 保存原始文件对象
};
});
// 添加到文件列表
self.files = self.files.concat(tempFiles);
self.renderFileList();
self.updateToolbarState();
if (typeof self.options.onSelect === 'function') {
self.options.onSelect(validFiles);
}
}
// 重置文件输入框,以便可以重复选择相同的文件
self.$fileInput.val('');
});
},
// =====================
// 文件判断
// =====================
isImageFile: function (filename) {
if (!filename || typeof filename !== 'string') return false;
const ext = filename.slice(filename.lastIndexOf('.') + 1).toLowerCase();
const imageExts = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']);
return imageExts.has(ext);
},
// =====================
// 更新工具栏状态
// =====================
updateToolbarState: function () {
var hasSelection = this.selectedFiles.size > 0;
this.$btnDownload.prop('disabled', !hasSelection);
this.$btnDelete.prop('disabled', !hasSelection);
var allSelected =
this.files.length > 0 &&
this.selectedFiles.size === this.files.length;
this.$checkAll.prop('checked', allSelected);
// 更新未上传文件数量
var pendingCount = this.files.filter(file => !file.uploaded).length;
this.$pendingCount.text(pendingCount);
var self = this;
this.$fileList.find('.file-item').each(function () {
var id = $(this).data('id');
var isSelected = self.selectedFiles.has(id);
$(this).find('.file-checkbox')
.prop('checked', isSelected);
// 添加或移除选中样式
if (isSelected) {
$(this).addClass('file-item-selected');
} else {
$(this).removeClass('file-item-selected');
}
});
},
// =====================
// 加载文件
// =====================
loadFiles: function (files) {
var self = this;
this.files = files.map(function (file) {
return Object.assign({}, file, {
_id: self.generateUniqueId(),
uploaded: true
});
});
this.selectedFiles.clear();
this.renderFileList();
this.updateToolbarState();
},
// =====================
// 渲染文件列表
// =====================
renderFileList: function () {
var self = this;
if (this.files.length === 0) {
this.$fileList.hide();
this.$emptyTip.show();
return;
}
this.$fileList.show();
this.$emptyTip.hide();
var html = '';
$.each(this.files, function (_, file) {
html += self.createFileItem(file);
});
this.$fileList.html(html);
this.bindFileItemEvents();
},
// =====================
// 格式化文件大小
// =====================
formatFileSize: function (bytes) {
if (bytes === null || bytes === undefined || isNaN(bytes) || bytes <= 0) {
return '-';
}
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
i = Math.max(0, Math.min(i, sizes.length - 1));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
},
// =====================
// 格式化日期
// =====================
formatDate: function (dateStr) {
if (!dateStr) return '-';
var date = new Date(dateStr);
if (isNaN(date.getTime())) {
return '-';
}
var year = date.getFullYear();
var month = ('0' + (date.getMonth() + 1)).slice(-2);
var day = ('0' + date.getDate()).slice(-2);
var hours = ('0' + date.getHours()).slice(-2);
var minutes = ('0' + date.getMinutes()).slice(-2);
var seconds = ('0' + date.getSeconds()).slice(-2);
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
},
// =====================
// 创建文件项
// =====================
createFileItem: function (file) {
const perm = this.options.permissions || {};
const downloadDisabled = !perm.download;
const deleteDisabled = !perm.delete;
let content = '';
if (this.isImageFile(file.name)) {
content = `
`;
} else {
content = this.getFileIcon(file.name);
}
// 未上传标记
const pendingBadge = file.uploaded ? '' : `
${this.language.pending}
`;
return `
${content}
${pendingBadge}
${file.name}
${this.formatFileSize(file.size)}
${this.formatDate(file.uploadDate)}
${this.staticIconMap.download}
${this.staticIconMap.delete}
${this.staticIconMap.more}
`;
},
// =====================
// 绑定文件项事件
// =====================
bindFileItemEvents: function () {
var self = this;
// =====================
// 文件item点击事件
// =====================
this.$fileList.off('click', '.file-item')
.on('click', '.file-item', function (e) {
var $checkbox = $(this).find('.file-checkbox');
$checkbox.prop('checked', !$checkbox.is(':checked'));
$checkbox.trigger('change');
});
this.$fileList.off('click', '.download')
.on('click', '.download', function (e) {
e.preventDefault();
e.stopPropagation();
var id = $(this).closest('.file-item').data('id');
var file = self.files.find(f => f._id === id);
if (!file) return;
if (typeof self.options.onDownload === 'function') {
self.options.onDownload([file]);
return;
}
self.internalDownload(file);
});
this.$fileList.off('change', '.file-checkbox')
.on('change', '.file-checkbox', function () {
var id = $(this).closest('.file-item').data('id');
if ($(this).is(':checked')) {
self.selectedFiles.add(id);
} else {
self.selectedFiles.delete(id);
}
self.updateToolbarState();
});
this.$fileList.off('click', '.delete')
.on('click', '.delete', function (e) {
e.preventDefault();
e.stopPropagation();
var id = $(this).closest('.file-item').data('id');
var file = self.files.find(f => f._id === id);
if (!file) return;
if (typeof self.options.onDelete === 'function') {
self.options.onDelete([file]);
return;
}
self.internalDelete(file);
});
// =====================
// 更多按钮点击事件(阻止冒泡)
// =====================
this.$fileList.off('click', '.more')
.on('click', '.more', function (e) {
e.preventDefault();
e.stopPropagation();
});
},
getUniqueFileName: function (fileName, files) {
// 保存原始文件名
const originalFileName = fileName;
let uniqueFileName = fileName;
let counter = 1;
// 如果文件名已经存在,则在文件名后加上 (1), (2), ...
while (files.has(uniqueFileName)) {
// 基于原始文件名为基础来添加计数器
const fileNameWithoutExtension = originalFileName.replace(/\.[^/.]+$/, "");
const extension = originalFileName.slice(originalFileName.lastIndexOf("."));
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`;
counter++;
}
return uniqueFileName;
},
getFileBlob: async function (url) {
if (!url) {
throw new Error('无效的文件URL');
}
const res = await fetch(url);
if (!res.ok) {
throw new Error('网络请求失败,状态码:' + res.status);
}
// ⭐ 比 blob 更轻一点(后续 zip 更快)
const buffer = await res.arrayBuffer();
return buffer;
},
// =====================
// 内部下载文件
// =====================
internalDownload: async function (file) {
if (!file) {
console.warn('下载失败:文件对象无效', file);
return;
}
console.log('开始下载文件:', file.name);
try {
let blob;
if (file.uploaded && file.url) {
// 已上传的文件,从URL下载
const buffer = await this.getFileBlob(file.url);
blob = new Blob([buffer]);
} else if (file.file) {
// 未上传的文件,使用本地文件对象
blob = file.file;
} else {
console.warn('下载失败:文件没有URL或本地文件对象', file);
return;
}
saveAs(blob, file.name || '未命名文件');
} catch (err) {
console.error('文件下载失败:', err);
}
},
// =====================
// 内部下载所有文件
// =====================
internalDownloadBatch: async function (files, callback) {
if (!files || files.length === 0) return;
const zip = new JSZip();
const addedFiles = new Set();
const total = files.length;
let current = 0;
let failedCount = 0;
const concurrency = 3;
const downloadOne = async (item) => {
try {
let content;
if (item.uploaded && item.url) {
// 已上传的文件,从URL下载
content = await this.getFileBlob(item.url);
} else if (item.file) {
// 未上传的文件,使用本地文件对象
content = await item.file.arrayBuffer();
} else {
console.warn('跳过失败文件:', item.name, '没有URL或本地文件对象');
failedCount++;
return;
}
const uniqueFileName = this.getUniqueFileName(item.name, addedFiles);
zip.file(uniqueFileName, content);
addedFiles.add(uniqueFileName);
} catch (err) {
console.warn('跳过失败文件:', item.name, err);
failedCount++;
} finally {
current++;
}
};
const runner = async () => {
while (files.length) {
const item = files.shift();
await downloadOne(item);
}
};
try {
await Promise.all(
Array.from({ length: concurrency }, runner)
);
const blob = await zip.generateAsync({ type: "blob" });
const now = new Date();
const pad = n => n < 10 ? "0" + n : n;
const fileName =
now.getFullYear() +
pad(now.getMonth() + 1) +
pad(now.getDate()) +
pad(now.getHours()) +
pad(now.getMinutes());
saveAs(blob, fileName + ".zip");
console.log(
`下载完成:${total - failedCount}/${total},失败:${failedCount}`
);
callback && callback();
} catch (err) {
console.error("Batch download failed:", err);
callback && callback();
}
},
// =====================
// 内部删除文件
// =====================
internalDelete: function (file) {
if (!file) return;
// 清理本地临时连接
if (file.url && file.uploaded === false) {
try {
URL.revokeObjectURL(file.url);
} catch (e) {
console.warn('Failed to revoke object URL:', e);
}
}
this.files = this.files.filter(f => f._id !== file._id);
this.selectedFiles.delete(file._id);
this.updateToolbarState();
this.renderFileList();
},
// =====================
// 内部删除所有文件
// =====================
internalDeleteBatch: function (files) {
if (!files || files.length === 0) return;
files.forEach(file => this.internalDelete(file));
this.updateToolbarState();
this.renderFileList();
},
// =====================
// 更新权限
// =====================
updatePermissions: function (permissions) {
if (!permissions) return;
this.options.permissions = permissions;
this.$btnSelect?.prop('disabled', !permissions.select);
this.$btnUpload?.prop('disabled', !permissions.upload);
this.$btnDownload?.prop('disabled', !permissions.download);
this.$btnDelete?.prop('disabled', !permissions.delete);
this.$fileList.find('.file-action.download')
.toggleClass('is-disabled', !permissions.download);
this.$fileList.find('.file-action.delete')
.toggleClass('is-disabled', !permissions.delete);
},
};
// ==========================
// jQuery 插件封装
// ==========================
$.fn.G3File = function (options) {
var args = Array.prototype.slice.call(arguments, 1);
var instance;
this.each(function () {
var $this = $(this);
instance = $this.data('G3File');
// ========= 初始化 =========
if (!instance) {
instance = Object.create(G3File);
instance.init(this.id, options);
$this.data('G3File', instance);
// ========= 方法调用 =========
} else if (typeof options === 'string') {
var method = options;
if (instance[method]) {
instance[method].apply(instance, args);
}
}
});
// ⭐关键:返回 instance(支持 file.xxx)
return instance || this;
};
})(jQuery);