(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 = `
${this.language.selectAll}
${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 = `${file.name}`; } 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);