(function ($) { "use strict"; // ============================================================================ // G3File 构造函数 // ============================================================================ function G3File(container, options) { // 实例隔离 this.$container = $(container); this.options = $.extend({}, this.defaults, options); this.instanceId = G3FileUtils.generateUniqueId(); this.documentNamespace = ".g3file_" + this.instanceId; this.files = []; this.selectedFiles = new Set(); this.objectUrls = new Map(); this.language = G3FileLanguages[this.options.language] || G3FileLanguages["zh-CN"]; // 操作菜单状态 this.actionMenuState = { open: false, fileId: null, anchorEl: null, items: [], }; // 当前任务(单个任务实例) this.tasks = new Map(); // UI锁定状态 this.uiLocked = false; // 阿里云 OSS 凭证缓存 this.aliyunKeyCache = null; // 预览相关状态(基于 fileId 定位,避免删除/新增导致错位) this.currentPreviewFileId = null; this.init(); } // ============================================================================ // 原型方法 // ============================================================================ G3File.prototype = { // ============================================================================ // 配置和常量 // ============================================================================ /** * 默认配置 */ defaults: { /** * 初始化时的文件数据 * @typedef {Object} FileItem * @property {string} _id - 文件唯一标识符 * @property {string} name - 文件名 * @property {number} size - 文件大小(字节) * @property {string} uploadDate - 上传日期(ISO格式) * @property {string} url - 文件访问URL * @property {File|null} file - 本地File对象(仅待上传文件有) * @property {string} status - 文件状态:pending/uploading/uploaded/failed * @type {FileItem[]} */ data: [], mode: "aliyun", onBeforeDelete: null, onDelete: null, onDownload: null, onSelect: null, onUpload: null, onUploadSuccess: null, onUploadError: null, onUploadComplete: null, onPreview: null, getActionMenu: null, language: "zh-CN", maxFileSize: 10 * 1024 * 1024, // 上传配置 uploadConfig: { aliyun: { // 阿里云配置是可选的 // getAliyunKey 是必须的,用于动态获取阿里云 OSS 上传凭证 getAliyunKey: null, // 异步函数,返回 { accessid, callback, dir, expire, host, policy, signature } }, customUpload: null, // 自定义上传方法 }, // 全局权限(支持对象或函数) globalPermission: { select: true, upload: true, download: true, delete: true, view: true, }, // 单文件权限解析函数(仅支持函数) filePermissionResolver: null, }, // ============================================================================ // 初始化和核心方法 // ============================================================================ /** * 初始化 * @returns {G3File} 实例 */ init: function () { var self = this; self.render(); self.bindEvents(); self.updateUI(); if (self.options.data && self.options.data.length) { self.setFiles(self.options.data); } self.preloadAliyunKey(); return this; }, /** * 预加载阿里云 OSS 凭证(初始化时调用) */ preloadAliyunKey: async function () { if (this.options.mode !== "aliyun") { return; } const aliyunConfig = this.options.uploadConfig.aliyun; if (!aliyunConfig || typeof aliyunConfig.getAliyunKey !== "function") { return; } try { const keyData = await aliyunConfig.getAliyunKey(); if (keyData && keyData.accessid && keyData.host) { this.aliyunKeyCache = { data: keyData, expireTime: keyData.expire ? keyData.expire * 1000 : Date.now() + 3600000, }; console.log("[G3File] 阿里云 OSS 凭证预加载成功"); } } catch (error) { console.warn("[G3File] 阿里云 OSS 凭证预加载失败:", error); } }, /** * 检查并获取有效的阿里云 OSS 凭证 * @returns {Object|null} 阿里云凭证数据 */ getValidAliyunKey: async function () { const now = Date.now(); if (this.aliyunKeyCache) { if (now < this.aliyunKeyCache.expireTime - 60000) { return this.aliyunKeyCache.data; } console.log("[G3File] 阿里云 OSS 凭证即将过期,自动刷新"); } const aliyunConfig = this.options.uploadConfig.aliyun; if (!aliyunConfig || typeof aliyunConfig.getAliyunKey !== "function") { throw new Error("阿里云上传配置不完整,缺少 getAliyunKey 函数"); } try { const keyData = await aliyunConfig.getAliyunKey(); if (!keyData || !keyData.accessid || !keyData.host) { throw new Error("阿里云凭证获取失败"); } this.aliyunKeyCache = { data: keyData, expireTime: keyData.expire ? keyData.expire * 1000 : Date.now() + 3600000, }; console.log("[G3File] 阿里云 OSS 凭证获取成功"); return keyData; } catch (error) { console.error("[G3File] 获取阿里云 OSS 凭证失败:", error); throw error; } }, /** * 生成文件 key(UUID + 原扩展名) * @param {Object} keyData - 阿里云凭证数据 * @param {string} filename - 原始文件名 * @returns {string} 生成的 key */ generateFileKey: function (keyData, filename) { const dir = keyData.dir || ""; const ext = filename.slice(filename.lastIndexOf(".")) || ""; const uuid = G3FileUtils.generateUniqueId().replace("file_", ""); return dir + uuid + ext; }, // ============================================================================ // 工具方法 // ============================================================================ /** * 获取文件图标 * @param {string} filename - 文件名 * @returns {string} 图标HTML */ getFileIcon: function (filename) { return G3FileUtils.getFileIcon(filename); }, /** * 获取文件Blob * @param {string} url - 文件URL * @returns {Promise} 文件二进制数据 */ getFileBlob: async function (url) { if (!url) { throw new Error("无效的文件URL"); } let fetchUrl = url; if (this.options.mode === "aliyun" && this.aliyunKeyCache) { const { accessid, policy, signature } = this.aliyunKeyCache.data; if (accessid && policy && signature) { const separator = url.includes("?") ? "&" : "?"; fetchUrl = `${url}${separator}OSSAccessKeyId=${accessid}&policy=${policy}&Signature=${signature}`; } } const res = await fetch(fetchUrl); if (!res.ok) { throw new Error("网络请求失败,状态码:" + res.status); } const buffer = await res.arrayBuffer(); return buffer; }, // ============================================================================ // UI 渲染相关方法 // ============================================================================ /** * 渲染 * @returns {G3File} 实例 */ render: function () { const html = `
${this.language.selectAll}
${this.language.emptyTip1} ${this.language.emptyTip2} ${this.language.emptyTip3}
0/0
${this.language.selectFiles}

${this.language.filePreview}

`; this.$container.html(html); this.$fileList = this.$container.find(".g3-main"); this.$emptyTip = this.$container.find(".g3-empty-tip"); this.$checkAll = this.$container.find(".g3-check-all-checkbox"); this.$btnDownload = this.$container.find(".g3-btn-download-all"); this.$btnDelete = this.$container.find(".g3-btn-delete-all"); this.$btnUpload = this.$container.find(".g3-btn-upload"); this.$btnSelect = this.$container.find(".g3-btn-select"); this.$fileInput = this.$container.find(".g3-file-input"); this.$pendingCount = this.$container.find(".g3-pending-number"); this.$previewPanel = this.$container.find(".g3-preview-panel"); this.$previewToolbar = this.$container.find(".g3-preview-toolbar"); this.$previewContent = this.$container.find(".g3-preview-content"); this.$btnPrev = this.$container.find(".g3-preview-prev"); this.$btnNext = this.$container.find(".g3-preview-next"); this.$previewName = this.$container.find(".g3-preview-name"); this.$previewCounter = this.$container.find(".g3-preview-counter"); return this; }, /** * 渲染操作菜单 */ renderActionMenu: function () { var actionMenu = this.$container.find(".g3-global-menu"); if (!actionMenu.length) { this.$container.append('
'); actionMenu = this.$container.find(".g3-global-menu"); } var actionMenuState = this.actionMenuState; if (!actionMenuState.open) { actionMenu.removeClass("is-open"); return; } var html = ""; actionMenuState.items.forEach(function (item) { var isDisabled = false; if (typeof item.disabled === "function") { isDisabled = item.disabled(self.getFileById(actionMenuState.fileId)); } else if (item.disabled !== undefined) { isDisabled = item.disabled; } var disabledClass = isDisabled ? "is-disabled" : ""; html += `
${item.title}
`; }); actionMenu.html(html); actionMenu .find(".g3-action-menu-item") .off("click") .on("click", function (e) { e.stopPropagation(); var idx = $(this).index(); var item = actionMenuState.items[idx]; var isDisabled = false; if (typeof item.disabled === "function") { isDisabled = item.disabled( self.getFileById(actionMenuState.fileId), ); } else if (item.disabled !== undefined) { isDisabled = item.disabled; } if (isDisabled) return; item.onClick && item.onClick(self.getFileById(actionMenuState.fileId)); self.closeActionMenu(); }); var $anchor = $(actionMenuState.anchorEl).closest(".g3-action-more"); var rect = $anchor[0].getBoundingClientRect(); var $mainContainer = this.$container.find(".main"); var mainRect = $mainContainer[0].getBoundingClientRect(); var menuWidth = 160; var itemHeight = 36; var menuHeight = actionMenuState.items.length * itemHeight; var top = rect.bottom; var left = rect.right - menuWidth; var spaceBottom = mainRect.bottom - rect.bottom; // main 容器底部到锚点的距离 var spaceTop = rect.top - mainRect.top; // 锚点到 main 容器顶部的距离 var openUp = spaceBottom < menuHeight && spaceTop > menuHeight; if (openUp) { top = rect.top - menuHeight; // 在锚点上方显示 } // 确保菜单不超出 main 容器左右边界 if (left < mainRect.left) { left = mainRect.left; } if (left + menuWidth > mainRect.right) { left = mainRect.right - menuWidth; } // 确保菜单不超出 main 容器上下边界 if (top < mainRect.top) { top = mainRect.top; } if (top + menuHeight > mainRect.bottom) { top = mainRect.bottom - menuHeight; } actionMenu.css({ top: top, left: left, }); requestAnimationFrame(function () { actionMenu.addClass("is-open"); }); }, /** * 创建文件项 * @param {Object} file - 文件对象 * @returns {string} 文件项HTML */ createFileItem: function (file) { const downloadDisabled = !this.hasPermission(file, "download"); const previewDisabled = !this.hasPermission(file, "view"); const deleteDisabled = !this.hasPermission(file, "delete"); let content = ""; if (G3FileUtils.isImageFile(file.name)) { content = `
${file.name}
${G3FileIcons.preview.imgLoadError}
`; } else { content = this.getFileIcon(file.name); } let statusBadge = ""; switch (file.status) { case "pending": statusBadge = `
${this.language.pending}
`; break; case "uploading": statusBadge = `
${this.language.uploading}
`; break; case "failed": statusBadge = `
${this.language.uploadFailed}
`; break; } return `
${content}
${statusBadge}
${G3FileIcons.action.delete}
${file.name} ${typeof file.size === "string" ? file.size || "-" : G3FileUtils.formatFileSize(file.size)} ${G3FileUtils.formatDate(file.uploadDate)}
${G3FileIcons.action.preview}
${G3FileIcons.action.download}
${G3FileIcons.action.more}
`; }, /** * 渲染文件列表 */ renderFileList: function () { if (this.files.length === 0) { this.$fileList.hide(); this.$emptyTip.show(); return; } this.$fileList.show(); this.$emptyTip.hide(); var html = ""; var self = this; this.files.forEach(function (file) { html += self.createFileItem(file); }); this.$fileList.html(html); }, /** * 更新UI */ updateUI: function () { var self = this; var hasSelectedFiles = this.selectedFiles.size > 0; // 更新全局按钮状态 this.$btnSelect?.prop("disabled", !this.hasPermission(null, "select")); this.$btnUpload?.prop("disabled", !this.hasPermission(null, "upload")); // 批量下载按钮:需要全局权限且有选中文件 var canBatchDownload = this.hasPermission(null, "download") && hasSelectedFiles; this.$btnDownload?.prop("disabled", !canBatchDownload); // 批量删除按钮:需要全局权限且有选中文件 var canBatchDelete = this.hasPermission(null, "delete") && hasSelectedFiles; this.$btnDelete?.prop("disabled", !canBatchDelete); // 更新全选checkbox状态 var allSelected = this.files.length > 0 && this.selectedFiles.size === this.files.length; this.$checkAll?.prop("checked", allSelected); // 更新待上传数量 var pendingCount = this.files.filter( (file) => file.status === "pending", ).length; this.$pendingCount?.text(pendingCount); // 更新文件项状态 this.$fileList.find(".g3-file-item").each(function () { var $item = $(this); var fileId = $item.data("id"); var file = self.files.find((f) => f._id === fileId); if (!file) return; // 更新checkbox选中状态 var isSelected = self.selectedFiles.has(fileId); $item.find(".g3-file-checkbox").prop("checked", isSelected); // 更新按钮权限状态 var canDownload = self.hasPermission(file, "download"); var canView = self.hasPermission(file, "view"); var canDelete = self.hasPermission(file, "delete"); $item .find(".g3-action-download") .toggleClass("is-disabled", !canDownload); $item.find(".g3-action-preview").toggleClass("is-disabled", !canView); $item .find(".g3-file-delete-btn") .toggleClass("is-disabled", !canDelete); }); }, updateFileItem: function (fileId) { var file = this.getFileById(fileId); var $item = this.$fileList.find(`.g3-file-item[data-id="${fileId}"]`); if (!file || !$item.length) return; $item .find(".g3-file-checkbox") .prop("checked", this.selectedFiles.has(fileId)); var $badge = $item.find(".g3-status-badge"); if (file.status === "pending") { $badge .attr("class", "g3-status-badge g3-status-pending") .text(this.language.pending) .show(); } else if (file.status === "uploading") { $badge .attr("class", "g3-status-badge g3-status-uploading") .text(this.language.uploading) .show(); } else if (file.status === "failed") { $badge .attr("class", "g3-status-badge g3-status-failed") .text(this.language.uploadFailed) .show(); } else if (file.status === "uploaded") { $badge.attr("class", "g3-status-badge g3-status-uploaded").hide(); } var canDownload = this.hasPermission(file, "download"); var canView = this.hasPermission(file, "view"); var canDelete = this.hasPermission(file, "delete"); $item .find(".g3-action-download") .toggleClass("is-disabled", !canDownload); $item.find(".g3-action-preview").toggleClass("is-disabled", !canView); $item.find(".g3-file-delete-btn").toggleClass("is-disabled", !canDelete); }, /** * 获取已上传的文件列表(仅status为uploaded的文件) */ getUploadedFiles: function () { return this.files.filter(function (file) { return file.status === "uploaded"; }); }, isPreviewableFile: function (fileName) { var nonPreviewableExts = [ ".rar", ".jar", ".exe", ".zip", ".7z", ".tar", ".gz", ".bz2", ".iso", ".dll", ".bin", ".msi", ".cab", ".rpm", ".deb", ".apk", ".ipa", ".dat", ".bak", ".tmp", ".log", ".cache", ".db", ".sqlite", ]; var ext = fileName.toLowerCase().substring(fileName.lastIndexOf(".")); return !nonPreviewableExts.includes(ext); }, getPreviewFileIndex: function (fileId) { var uploadedFiles = this.getUploadedFiles(); return uploadedFiles.findIndex(function (f) { return f._id === fileId; }); }, previewPrev: function () { var uploadedFiles = this.getUploadedFiles(); if (uploadedFiles.length === 0) return; var currentIndex = this.getPreviewFileIndex(this.currentPreviewFileId); var newIndex = currentIndex > 0 ? currentIndex - 1 : uploadedFiles.length - 1; this.currentPreviewFileId = uploadedFiles[newIndex]._id; this.updatePreview(); }, previewNext: function () { var uploadedFiles = this.getUploadedFiles(); if (uploadedFiles.length === 0) return; var currentIndex = this.getPreviewFileIndex(this.currentPreviewFileId); var newIndex = currentIndex < uploadedFiles.length - 1 ? currentIndex + 1 : 0; this.currentPreviewFileId = uploadedFiles[newIndex]._id; this.updatePreview(); }, /** * 更新预览区域显示 */ updatePreview: function () { var allFiles = this.getUploadedFiles(); var total = allFiles.length; if (total === 0) { this.$previewContent.html(`

${this.language.selectFiles}

`); this.$previewName.text(this.language.selectFiles); this.$previewCounter.text("0/0"); this.$btnPrev.prop("disabled", true); this.$btnNext.prop("disabled", true); this.currentPreviewFileId = null; return; } if ( !this.currentPreviewFileId || !this.getFileById(this.currentPreviewFileId) ) { this.currentPreviewFileId = allFiles[0]._id; } var currentFile = this.getFileById(this.currentPreviewFileId); if (!currentFile) { this.currentPreviewFileId = allFiles[0]._id; currentFile = allFiles[0]; } var currentIndex = this.getPreviewFileIndex(this.currentPreviewFileId); var isUploaded = currentFile.status === "uploaded"; var isPreviewable = this.isPreviewableFile(currentFile.name); var fileUrl = currentFile.url && currentFile.url !== "" ? currentFile.url : null; this.$btnPrev.prop("disabled", currentIndex === 0); this.$btnNext.prop("disabled", currentIndex === total - 1); this.$previewName.text(currentFile.name); var counterText = this.language.previewCount .replace("{current}", currentIndex + 1) .replace("{total}", total); this.$previewCounter.text(counterText); var previewContent = ""; if (isUploaded && fileUrl && isPreviewable) { var previewUrl = this.options.previewUrl || ""; var fullPreviewUrl = previewUrl ? previewUrl + G3FileUtils.base64Encode(fileUrl) : fileUrl; previewContent = ` `; } else { var tipsHtml = ""; if (!isUploaded) { tipsHtml = `

${this.language.uploadToPreview}

`; } else if (!isPreviewable) { tipsHtml = `

${this.language.downloadToPreview}

`; } else if (!fileUrl) { tipsHtml = `

${this.language.urlError}

`; } previewContent = `
${tipsHtml}
`; } this.$previewContent.html(previewContent); }, setPreviewFile: function (fileId) { var file = this.getFileById(fileId); if (file) { this.currentPreviewFileId = fileId; this.updatePreview(); } }, // ============================================================================ // 事件绑定相关方法 // ============================================================================ /** * 绑定事件 */ bindEvents: function () { var self = this; var documentActionMenuEvent = "click.g3file_actionmenu_" + this.instanceId; var documentPasteEvent = "paste" + this.documentNamespace; // 选择全部 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.updateUI(); }); // 批量下载 this.$btnDownload.off("click").on("click", function () { if (!self.hasPermission(null, "download")) return; var selectedFiles = self.getSelectedFiles(); if (selectedFiles.length === 0) return; self.handleDownload(selectedFiles, { batch: true }); }); // 批量删除 this.$btnDelete.off("click").on("click", function () { if (!self.hasPermission(null, "delete")) return; var selectedFiles = self.getSelectedFiles(); if (selectedFiles.length === 0) return; self.handleDelete(selectedFiles); }); // 选择文件 this.$btnSelect.off("click").on("click", function () { if (!self.hasPermission(null, "select")) return; self.$fileInput.click(); }); // 上传 this.$btnUpload.off("click").on("click", function () { if (!self.hasPermission(null, "upload")) return; var pendingFiles = self.files.filter( (file) => file.status === "pending", ); if (typeof self.options.onUpload === "function") { self.options.onUpload(pendingFiles); } else { // 使用新的上传逻辑 self.handleUpload(self.files); } }); // 文件选择 this.$fileInput.off("change").on("change", function (e) { var files = e.target.files; if (!files || files.length === 0) return; self.handleFileSelect(files); // 重置文件输入框 self.$fileInput.val(""); }); // 点击其他地方关闭菜单 $(document) .off(documentActionMenuEvent) .on(documentActionMenuEvent, function (e) { if ( !$(e.target).closest(".g3-action-more").length && !$(e.target).closest(".g3-global-menu").length ) { self.closeActionMenu(); } }); // 拖拽文件 const $container = this.$container; $container .off("dragenter dragleave dragover drop") .on("dragenter", function (e) { e.preventDefault(); e.stopPropagation(); $container.addClass("drag-over"); }) .on("dragover", function (e) { e.preventDefault(); e.stopPropagation(); $container.addClass("drag-over"); }) .on("dragleave", function (e) { e.preventDefault(); e.stopPropagation(); if (e.target === $container[0]) { $container.removeClass("drag-over"); } }) .on("drop", function (e) { e.preventDefault(); e.stopPropagation(); $container.removeClass("drag-over"); var files = e.originalEvent.dataTransfer.files; if (files && files.length > 0) { self.handleFileSelect(files); } }); // 粘贴文件 $(document) .off(documentPasteEvent) .on(documentPasteEvent, function (e) { var clipboardData = e.originalEvent.clipboardData; var files = clipboardData.files; if (files && files.length > 0) { e.preventDefault(); self.handleFileSelect(files); } }); // 预览上一个 this.$btnPrev.off("click").on("click", function () { self.previewPrev(); }); // 预览下一个 this.$btnNext.off("click").on("click", function () { self.previewNext(); }); // 文件列表事件委托(只在 init 时绑定一次) this.$fileList // 预览按钮 .on("click", ".g3-action-preview", function (e) { e.preventDefault(); e.stopPropagation(); var id = $(this).closest(".g3-file-item").data("id"); var file = self.getFileById(id); if (!file) return; if (!self.hasPermission(file, "view")) return; self.setPreviewFile(id); if (typeof self.options.onPreview === "function") { self.options.onPreview([file]); } }) // 下载按钮 .on("click", ".g3-action-download", function (e) { e.preventDefault(); e.stopPropagation(); var id = $(this).closest(".g3-file-item").data("id"); var file = self.getFileById(id); if (!file) return; if (!self.hasPermission(file, "download")) return; self.handleDownload([file], { batch: false }); }) // 复选框 .on("change", ".g3-file-checkbox", function () { var id = $(this).closest(".g3-file-item").data("id"); if ($(this).is(":checked")) { self.selectedFiles.add(id); } else { self.selectedFiles.delete(id); } self.updateUI(); }) // 删除按钮 .on("click", ".g3-file-delete-btn", function (e) { e.preventDefault(); e.stopPropagation(); var id = $(this).closest(".g3-file-item").data("id"); var file = self.getFileById(id); if (!file) return; if (!self.hasPermission(file, "delete")) return; self.handleDelete([file]); }) // 文件项单击预览 .on("click", ".g3-file-item", function (e) { if ( $(e.target).closest( ".g3-file-action, .g3-file-checkbox, .g3-file-delete-btn", ).length ) { return; } var id = $(this).data("id"); var file = self.getFileById(id); if (!file) return; if (!self.hasPermission(file, "view")) return; if (typeof self.options.onPreview === "function") { self.options.onPreview([file]); } self.setPreviewFile(id); }) // 更多按钮 .on("click", ".g3-action-more", function (e) { e.preventDefault(); e.stopPropagation(); var $el = $(this).closest(".g3-file-action"); var id = $el.closest(".g3-file-item").data("id"); var file = self.getFileById(id); if (!file) return; if ( self.actionMenuState.open && self.actionMenuState.fileId === file._id ) { self.closeActionMenu(); return; } self.openActionMenu($el, file); }); // 任务关闭按钮事件委托 this.$container.on("click", ".g3-task-close", function (e) { e.preventDefault(); e.stopPropagation(); var taskId = $(this).closest(".g3-task-center").data("task-id"); self.closeTask(taskId); }); }, // ============================================================================ // 文件处理相关方法 // ============================================================================ /** * 处理文件选择 * @param {FileList} files - 文件列表 */ handleFileSelect: async function (files) { var self = this; var maxSize = this.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 = this.language.fileSizeError || "文件大小超过限制"; console.warn( errorMsg + ": " + invalidFiles.map((f) => f.name).join(", "), ); } if (validFiles.length > 0) { var tempFiles = []; for (var j = 0; j < validFiles.length; j++) { var file = validFiles[j]; let url = ""; let compressedFile = file; if (G3FileUtils.isImageFile(file.name)) { if (typeof self.options.compressImage === "function") { try { compressedFile = await self.options.compressImage(file); } catch (e) { console.warn("外部压缩算法执行失败,使用内部压缩:", e); compressedFile = await G3FileUtils.compressImage(file); } } else { compressedFile = await G3FileUtils.compressImage(file); } url = URL.createObjectURL(compressedFile); self.objectUrls.set(G3FileUtils.generateUniqueId(), url); } tempFiles.push({ _id: G3FileUtils.generateUniqueId(), name: file.name, size: compressedFile.size, uploadDate: new Date().toISOString(), url: url, file: compressedFile, status: "pending", }); } self.setFiles(self.files.concat(tempFiles)); if (typeof self.options.onSelect === "function") { self.options.onSelect(validFiles); } self.autoScrollToBottom(); } }, /** * 自动滚动到底部 */ autoScrollToBottom: function () { this.$fileList.scrollTop(this.$fileList[0].scrollHeight); }, /** * 处理下载 * @param {Array} files - 文件数组 * @param {Object} options - 下载选项 */ handleDownload: function (files, options) { if (typeof this.options.onDownload === "function") { this.options.onDownload(files, options); return; } if (options.batch) { this.internalDownloadBatch(files); } else { this.internalDownload(files[0]); } }, /** * 处理删除 * @param {Array} files - 文件数组 */ handleDelete: function (files) { var self = this; if (this.uiLocked) { console.warn("UI is locked, please wait..."); return; } if (typeof this.options.onBeforeDelete === "function") { var result = this.options.onBeforeDelete(files); if (result === false) return; if (typeof result.then === "function") { result.then(function (confirmed) { if (confirmed) self.internalDelete(files); }); return; } if (typeof result === "function") { result(function (confirmed) { if (confirmed) self.internalDelete(files); }); return; } } else if (typeof this.options.onDelete === "function") { this.options.onDelete(files, function (success) { if (success) { self.internalDelete(files); } }); return; } this.internalDelete(files); }, /** * 内部下载 * @param {Object} file - 文件对象 */ internalDownload: async function (file) { if (!file) { console.warn("下载失败:文件对象无效", file); this.showToast({ title: this.language.error || "错误", message: this.language.downloadError || "下载失败,请检查文件是否有效", type: "error", duration: 3000, }); return; } console.log("开始下载文件:", file.name); try { let blob; if (file.status === "uploaded" && file.url) { const buffer = await this.getFileBlob(file.url); blob = new Blob([buffer]); } else if (file.file) { blob = file.file; } else { console.warn("下载失败:文件没有URL或本地文件对象", file); this.showToast({ title: this.language.error || "错误", message: this.language.downloadError || "下载失败,请检查文件是否有效", type: "error", duration: 3000, }); return; } saveAs(blob, file.name || "未命名文件"); } catch (err) { console.error("文件下载失败:", err); this.showToast({ title: this.language.error || "错误", message: this.language.downloadError || "下载失败,请检查文件是否有效", type: "error", duration: 3000, }); } }, /** * 内部批量下载 * @param {Array} files - 文件数组 */ internalDownloadBatch: async function (files) { if (!files || files.length === 0) return; const zip = new JSZip(); const addedFiles = new Set(); const total = files.length; let successCount = 0; let failedCount = 0; const concurrency = 3; const taskId = G3FileUtils.generateUniqueId(); this.startTask({ id: taskId, title: this.language.batchDownload || "批量下载", total: total, current: 0, currentItem: "", }); const downloadOne = async (item) => { try { this.updateTask({ id: taskId, current: successCount + failedCount, currentItem: item.name, }); let content; if (item.status === "uploaded" && item.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 = G3FileUtils.getUniqueFileName( item.name, addedFiles, ); zip.file(uniqueFileName, content); addedFiles.add(uniqueFileName); successCount++; } catch (err) { console.warn("跳过失败文件:", item.name, err); failedCount++; } }; try { const filesCopy = [...files]; const runner = async () => { while (filesCopy.length) { const item = filesCopy.shift(); await downloadOne(item); } }; await Promise.all(Array.from({ length: concurrency }, runner)); this.updateTask({ id: taskId, current: total, }); 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"); this.finishTask({ id: taskId, total: total, success: successCount, failed: failedCount, }); } catch (err) { console.error("批量下载失败:", err); failedCount = total - successCount; this.finishTask({ id: taskId, total: total, success: successCount, failed: failedCount, }); } }, /** * 内部删除 * @param {Array} files - 文件数组 */ internalDelete: function (files) { var self = this; files.forEach(function (file) { // 清理本地临时连接 if (file.url && file.status === "pending") { self.objectUrls.forEach((url, key) => { if (url === file.url) { try { URL.revokeObjectURL(url); self.objectUrls.delete(key); } catch (e) { console.warn("释放ObjectURL失败:", e); } } }); } self.selectedFiles.delete(file._id); }); this.files = this.files.filter( (f) => !files.find((file) => file._id === f._id), ); this.renderFileList(); this.updateUI(); }, /** * 更新文件状态 * @param {string} fileId - 文件ID * @param {string} status - 新状态 * @param {Object} data - 附加数据 */ updateFileStatus: function (fileId, status, data) { const file = this.files.find((f) => f._id === fileId); if (file) { file.status = status; if (data) { Object.assign(file, data); } this.updateFileItem(fileId); } }, /** * 内部上传方法,支持阿里云 OSS 直传 * @param {Object} file - 文件对象 * @param {Function} callback - 上传完成回调 */ internalUpload: async function (file, callback) { try { const keyData = await this.getValidAliyunKey(); const key = this.generateFileKey(keyData, file.name); const formData = new FormData(); formData.append("OSSAccessKeyId", keyData.accessid); formData.append("policy", keyData.policy); formData.append("signature", keyData.signature); formData.append("key", key); formData.append("callback", keyData.callback); formData.append("success_action_status", "200"); formData.append("file", file.file || file); const response = await fetch(keyData.host, { method: "POST", body: formData, }); if (!response.ok) { throw new Error(`上传失败,HTTP 状态码: ${response.status}`); } const result = await response.json(); const uploadedFile = { ...file, status: "uploaded", url: result.url || `${keyData.host}/${key}`, uploadDate: new Date().toISOString(), }; callback(null, uploadedFile); } catch (error) { console.error("[G3File] 上传失败:", error); callback(error); } }, /** * 处理上传逻辑 * @param {Array} files - 文件数组 */ handleUpload: function (files) { var self = this; if (this.uiLocked) { console.warn("UI is locked, please wait..."); return; } var customUpload = this.options.uploadConfig.customUpload; var concurrency = 3; var pendingFiles = files.filter(function (file) { return file.status === "pending"; }); if (pendingFiles.length === 0) { if (typeof self.options.onUploadComplete === "function") { self.options.onUploadComplete(0, 0); } return; } var total = pendingFiles.length; var taskId = this.startTask({ id: G3FileUtils.generateUniqueId(), title: this.language.batchUpload || "批量上传", total: total, current: 0, currentItem: "", }); var successCount = 0; var failCount = 0; var completedCount = 0; var index = 0; function uploadNext() { if (index >= pendingFiles.length) return Promise.resolve(); var file = pendingFiles[index++]; self.updateFileStatus(file._id, "uploading"); return new Promise(function (resolve) { function handleResult(error, uploadedFile) { completedCount++; if (error) { failCount++; self.updateFileStatus(file._id, "failed"); if (typeof self.options.onUploadError === "function") { self.options.onUploadError(file, error); } } else { successCount++; self.updateFileStatus(file._id, "uploaded", uploadedFile); if (typeof self.options.onUploadSuccess === "function") { self.options.onUploadSuccess(uploadedFile); } } self.updateTask({ id: taskId, current: completedCount, currentItem: file.name, }); if (completedCount === total) { self.finishTask({ id: taskId, total: total, success: successCount, failed: failCount, }); if (typeof self.options.onUploadComplete === "function") { self.options.onUploadComplete(successCount, failCount); } } resolve(); } if (typeof customUpload === "function") { customUpload(file) .then(function (uploadedFile) { handleResult(null, uploadedFile); }) .catch(function (error) { handleResult(error, null); }); } else if (self.options.mode === "aliyun") { self.internalUpload(file, function (error, uploadedFile) { handleResult(error, uploadedFile); }); } else { handleResult(new Error("No upload method configured"), null); } }).then(function () { return uploadNext(); }); } var runners = []; for (var i = 0; i < concurrency && i < pendingFiles.length; i++) { runners.push(uploadNext()); } Promise.all(runners); }, // ============================================================================ // Toast 提示系统 // ============================================================================ /** * Toast 图标映射(从外部 G3FileIcons 导入) */ toastIcons: { success: G3FileIcons.toast.success, error: G3FileIcons.toast.error, info: G3FileIcons.toast.info, warning: G3FileIcons.toast.warning, }, /** * 显示 Toast 提示 * @param {Object} options - Toast 配置 * @param {string} options.title - 标题 * @param {string} options.message - 消息内容 * @param {string} options.type - 类型:success/error/info/warning * @param {number} options.duration - 显示时长(毫秒),默认 3000 */ showToast: function (options) { const { title, message, type = "info", duration = 3000 } = options; const self = this; const toastContainer = this.$container.find(".g3-toast-container"); if (!toastContainer.length) { this.$container.append('
'); this.$toastContainer = this.$container.find(".g3-toast-container"); } const toastId = "toast_" + G3FileUtils.generateUniqueId(); const toastType = this.language.toast[type] || type; const icon = this.toastIcons[type] || this.toastIcons.info; const toastHtml = `
${icon}
${toastType}
${message ? `
${message}
` : ""}
`; this.$toastContainer.append(toastHtml); const $toast = this.$container.find(`#${toastId}`); $toast .find(".g3-toast-close") .off("click") .on("click", function () { self.removeToast(toastId); }); setTimeout(function () { self.removeToast(toastId); }, duration); const maxToasts = 5; const toasts = this.$toastContainer.find(".g3-toast"); if (toasts.length > maxToasts) { toasts.first().remove(); } }, /** * 移除 Toast * @param {string} toastId - Toast ID */ removeToast: function (toastId) { const $toast = this.$container.find(`#${toastId}`); if ($toast.length) { $toast.addClass("closing"); setTimeout(function () { $toast.remove(); }, 300); } }, // ============================================================================ // Task Center 任务中心系统 // ============================================================================ /** * 创建并启动任务 * @param {Object} options - 任务配置 * @param {string} options.id - 任务ID * @param {string} options.title - 任务标题(国际化key或文本) * @param {number} options.total - 总数量 * @param {number} options.current - 当前进度 * @param {string} options.currentItem - 当前处理项 */ startTask: function (options) { var task = { id: options.id || G3FileUtils.generateUniqueId(), title: options.title, total: options.total || 0, current: options.current || 0, currentItem: options.currentItem || "", success: 0, failed: 0, status: "running", }; this.tasks.set(task.id, task); this.lockUI(); this.renderTaskCenter(task.id); return task.id; }, /** * 更新任务进度 * @param {Object} options - 更新配置 * @param {string} options.id - 任务ID * @param {number} options.current - 当前进度 * @param {string} options.currentItem - 当前处理项 */ updateTask: function (options) { var task = this.tasks.get(options.id); if (!task) return; if (options.current !== undefined) task.current = options.current; if (options.currentItem !== undefined) task.currentItem = options.currentItem; this.updateTaskCenter(options.id); }, /** * 完成任务 * @param {Object} options - 完成配置 * @param {string} options.id - 任务ID * @param {number} options.total - 总数量 * @param {number} options.success - 成功数量 * @param {number} options.failed - 失败数量 */ finishTask: function (options) { var task = this.tasks.get(options.id); if (!task) return; task.status = "finished"; task.total = options.total !== undefined ? options.total : task.total; task.success = options.success || 0; task.failed = options.failed || 0; task.current = task.total; this.updateTaskCenter(options.id); this.tasks.delete(options.id); if (this.tasks.size === 0) { this.unlockUI(); } }, /** * 手动关闭任务 UI * @param {string} id - 任务ID */ closeTask: function (id) { var task = this.tasks.get(id); if (task) { task.status = "closed"; this.tasks.delete(id); } var taskCenter = this.$container.find( `.g3-task-center[data-task-id="${id}"]`, ); if (taskCenter.length) { taskCenter.remove(); } this.$taskCenter = null; if (this.tasks.size === 0) { this.unlockUI(); } }, renderTaskCenter: function (taskId) { var task = this.tasks.get(taskId); if (!task) return; let taskCenter = this.$container.find(".g3-task-center"); if (!taskCenter.length) { this.$container.append(`
${task.title}
`); taskCenter = this.$container.find(".g3-task-center"); } this.$taskCenter = taskCenter; this.$taskCenter.addClass("is-visible"); this.updateTaskCenter(taskId); }, updateTaskCenter: function (taskId) { var task = this.tasks.get(taskId); if (!task || !this.$taskCenter) return; const progress = task.total > 0 ? ((task.current / task.total) * 100).toFixed(0) : 0; this.$taskCenter.find(".g3-task-title").text(task.title); this.$taskCenter .find(".g3-current-item") .text( task.currentItem ? `${this.language.task.currentItem}: ${task.currentItem}` : "", ); this.$taskCenter.find(".g3-progress-inner").css("width", `${progress}%`); this.$taskCenter .find(".g3-progress-text") .text(`${task.current} / ${task.total}`); if (task.status === "finished") { this.$taskCenter.find(".g3-result-section").show(); this.$taskCenter .find(".g3-result-item:first .g3-result-value") .text(task.total); this.$taskCenter .find(".g3-result-success .g3-result-value") .text(task.success); this.$taskCenter .find(".g3-result-failed .g3-result-value") .text(task.failed); } else { this.$taskCenter.find(".g3-result-section").hide(); } }, // ============================================================================ // UI Lock 机制 // ============================================================================ /** * 锁定 UI */ lockUI: function () { this.uiLocked = true; let uiLock = this.$container.find(".g3-ui-lock"); if (!uiLock.length) { this.$container.append('
'); uiLock = this.$container.find(".g3-ui-lock"); } this.$uiLock = uiLock; this.$uiLock.addClass("is-locked"); }, /** * 解锁 UI */ unlockUI: function () { this.uiLocked = false; this.$uiLock?.removeClass("is-locked"); }, // ============================================================================ // 操作菜单相关方法 // ============================================================================ /** * 关闭操作菜单 */ closeActionMenu: function () { this.actionMenuState.open = false; this.actionMenuState.fileId = null; this.actionMenuState.anchorEl = null; this.actionMenuState.items = []; this.renderActionMenu(); }, /** * 打开操作菜单 * @param {jQuery} $el - 触发元素 * @param {Object} file - 文件对象 */ openActionMenu: function ($el, file) { var items = (this.options.getActionMenu && this.options.getActionMenu(file)) || []; this.actionMenuState = { open: true, fileId: file._id, anchorEl: $el[0], items: items, }; this.renderActionMenu(); }, // ============================================================================ // 权限相关方法 // ============================================================================ /** * 权限判断(唯一入口) * @param {Object} file - 文件对象 * @param {string} action - 操作类型 * @returns {boolean} 是否有权限 */ hasPermission: function (file, action) { return this.resolvePermission(file, action); }, resolvePermission: function (file, action) { var globalPermission = this.options.globalPermission; var filePermissionResolver = this.options.filePermissionResolver; if (typeof filePermissionResolver === "function") { var fileResult = filePermissionResolver({ file: file, action: action }); if (fileResult !== undefined) { return !!fileResult; } } if (typeof globalPermission === "function") { return !!globalPermission({ file: file, action: action }); } else if (typeof globalPermission === "object") { return !!globalPermission[action]; } return true; }, /** * 更新权限配置 * @param {Object} config - 权限配置 */ updatePermissions: function (config) { if (!config) return; if (config.globalPermission !== undefined) { this.options.globalPermission = config.globalPermission; } if (config.filePermissionResolver !== undefined) { this.options.filePermissionResolver = config.filePermissionResolver; } this.updateUI(); }, // ============================================================================ // 其他方法 // ============================================================================ /** * 根据ID获取文件 * @param {string} id - 文件ID * @returns {Object} 文件对象 */ getFileById: function (id) { return this.files.find(function (f) { return f._id === id; }); }, /** * 获取选中文件 * @returns {Array} 选中文件数组 */ getSelectedFiles: function () { return this.files.filter((file) => this.selectedFiles.has(file._id)); }, /** * 获取所有文件 * @returns {Array} 文件数组 */ getFiles: function () { return this.files; }, /** * 设置文件 * @param {Array} files - 文件数组 * @returns {G3File} 实例 */ setFiles: function (files) { this.files = files.map(function (file) { return { _id: file._id || G3FileUtils.generateUniqueId(), name: file.name, size: file.size, uploadDate: file.uploadDate || new Date().toISOString(), url: file.url || "", file: file.file || null, status: file.status || "uploaded", ...file, }; }); this.selectedFiles.clear(); this.currentPreviewFileId = null; this.renderFileList(); this.updateUI(); this.updatePreview(); return this; }, /** * 更新状态 * @param {Object} partialState - 部分状态 * @returns {G3File} 实例 */ updateState: function (partialState) { if (partialState.files) { this.setFiles(partialState.files); } else { // 其他状态更新时关闭所有运行中的任务 var self = this; this.tasks.forEach(function (task, taskId) { self.closeTask(taskId); }); } if (partialState.selectedFiles) { this.selectedFiles = new Set(partialState.selectedFiles); this.updateUI(); } if ( Object.prototype.hasOwnProperty.call( partialState, "globalPermission", ) || Object.prototype.hasOwnProperty.call( partialState, "filePermissionResolver", ) ) { this.updatePermissions({ globalPermission: partialState.globalPermission, filePermissionResolver: partialState.filePermissionResolver, }); } return this; }, /** * 销毁 */ destroy: function () { // 清理 document 事件 $(document).off(this.documentNamespace); $(document).off(".g3file_actionmenu_" + this.instanceId); // 清理 ObjectURL var self = this; this.objectUrls.forEach(function (url) { try { URL.revokeObjectURL(url); } catch (e) { console.warn("释放ObjectURL失败:", e); } }); this.objectUrls.clear(); // 清理任务 this.tasks.forEach(function (task, taskId) { self.closeTask(taskId); }); // 清理 DOM this.$container.find(".g3-global-menu").remove(); this.$container.find(".g3-toast-container").remove(); this.$container.find(".g3-task-center").remove(); this.$container.find(".g3-ui-lock").remove(); this.$container.empty(); // 重置状态 this.files = []; this.selectedFiles.clear(); this.tasks.clear(); this.currentPreviewFileId = null; this.uiLocked = false; console.log("G3File 组件已销毁"); }, }; // ============================================================================ // 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 = new G3File(this, options); $this.data("G3File", instance); } else if (typeof options === "string") { var method = options; if (instance[method]) { // 保存方法调用结果 methodResult = instance[method].apply(instance, args); } } }); if (typeof options === "string") { return methodResult !== undefined ? methodResult : this; } return instance || this; }; })(jQuery);