(function ($) { "use strict"; var G3File = { files: [], selectedFiles: new Set(), previewFiles: 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.selectedFiles = new Set(); // 选中文件集合 this.previewFiles = new Set(); // 预览文件集合 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, onPreview: 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}
`; // 预览状态(使用class标识) const isPreview = this.previewFiles.has(file._id); // 选中状态 const isSelected = this.selectedFiles.has(file._id); 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 target = $(e.target); // 如果点击的是 checkbox、download、delete、more 按钮,则不执行预览逻辑 if ( target.is(".file-checkbox") || target.closest(".file-action").length > 0 ) { return; } var id = $(this).data("id"); var file = self.files.find((f) => f._id === id); if (!file) return; // 切换预览状态(只允许同时预览一个文件) var isPreview = self.previewFiles.has(id); if (isPreview) { // 取消预览 self.previewFiles.delete(id); } else { // 开启预览前,先清除其他文件的预览状态 self.previewFiles.clear(); self.previewFiles.add(id); } // 重新渲染文件列表以更新预览状态 self.renderFileList(); // 调用外部回调,传递文件和预览状态 if (typeof self.options.onPreview === "function") { self.options.onPreview(file, !isPreview); } }); 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); }, // ===================== // 外部方法:切换文件选中状态 // 参数:fileId - 文件ID,isSelected - 是否选中(可选,不传则切换) // ===================== toggleSelect: function (fileId, isSelected) { var file = this.files.find((f) => f._id === fileId); if (!file) return; var currentSelected = this.selectedFiles.has(fileId); var newSelected = isSelected !== undefined ? isSelected : !currentSelected; if (newSelected) { this.selectedFiles.add(fileId); } else { this.selectedFiles.delete(fileId); } this.updateToolbarState(); this.renderFileList(); return newSelected; }, // ===================== // 外部方法:获取当前选中的文件列表 // ===================== getSelectedFiles: function () { return this.files.filter((f) => this.selectedFiles.has(f._id)); }, // ===================== // 外部方法:获取当前预览的文件列表 // ===================== getPreviewFiles: function () { return this.files.filter((f) => this.previewFiles.has(f._id)); }, // ===================== // 外部方法:获取所有文件列表 // ===================== getFiles: function () { return this.files; }, // ===================== // 外部方法:切换文件预览状态 // 参数:fileId - 文件ID,isPreview - 是否预览(可选,不传则切换) // ===================== togglePreview: function (fileId, isPreview) { var file = this.files.find((f) => f._id === fileId); if (!file) return; var currentPreview = this.previewFiles.has(fileId); var newPreview = isPreview !== undefined ? isPreview : !currentPreview; if (newPreview) { // 开启预览前,先清除其他文件的预览状态(只允许同时预览一个) this.previewFiles.clear(); this.previewFiles.add(fileId); } else { this.previewFiles.delete(fileId); } this.renderFileList(); // 调用外部回调 if (typeof this.options.onPreview === "function") { this.options.onPreview(file, newPreview); } return newPreview; }, }; // ========================== // jQuery 插件封装 // ========================== $.fn.G3File = function (options) { var args = Array.prototype.slice.call(arguments, 1); var instance; var methodResult; // 存储方法调用结果 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]) { // 保存方法调用结果 methodResult = instance[method].apply(instance, args); } } }); // 如果是方法调用,返回方法结果;否则返回实例 return methodResult !== undefined ? methodResult : instance || this; }; })(jQuery);