(function ($) { "use strict"; // ===================== // G3File 构造函数 // ===================== function G3File(container, options) { // 实例隔离 this.$container = $(container); this.options = $.extend({}, this.defaults, options); this.instanceId = this.generateUniqueId(); this.files = []; this.selectedFiles = new Set(); this.objectUrls = new Map(); this.language = this.languages[this.options.language] || this.languages['zh-CN']; this.actionMenuState = { open: false, fileId: null, anchorEl: null, items: [] }; // 任务管理 this.tasks = []; this.taskStats = { total: 0, success: 0, fail: 0 }; this.init(); } // ===================== // 原型方法 // ===================== G3File.prototype = { // ===================== // 默认配置 // ===================== defaults: { data: [], onDelete: null, onDownload: null, onSelect: null, onUpload: null, onPreview: null, getActionMenu: null, language: 'zh-CN', maxFileSize: 10 * 1024 * 1024, // 全局权限(支持对象或函数) globalPermission: { select: true, upload: true, download: true, delete: true, view: true }, // 单文件权限解析函数(仅支持函数) filePermissionResolver: null }, // ===================== // 多语言 // ===================== languages: { 'zh-CN': { selectAll: '全选', download: '下载', downloadBatch: "批量下载", delete: '删除', upload: '上传', select: '选择', more: '更多', confirmDelete: '确定要删除选中的 {count} 个文件吗?', downloading: '准备下载 {count} 个文件', deleting: '删除中...', name: '名称', size: '大小', date: '日期', pending: '未上传', selectFiles: '选择文件', emptyTip1: '拖拽文件到这里 …', emptyTip2: '支持多文件同时上传/下载', emptyTip3: '支持粘贴图片上传', fileSizeError: '文件大小超过限制', preview: '预览', downloadingStatus: '下载中:', downloadingFile: '正在下载:', downloadComplete: '下载完成', downloadResult: '共{total}文件,{success}成功,{failed}失败', close: '关闭' }, 'en-US': { selectAll: 'Select All', download: 'Download', downloadBatch: 'Batch Download', delete: 'Delete', upload: 'Upload', select: 'Select', more: 'More', confirmDelete: 'Are you sure you want to delete {count} selected file(s)?', downloading: 'Preparing to download {count} file(s)', deleting: 'Deleting...', name: 'Name', size: 'Size', date: 'Date', pending: 'Pending upload', selectFiles: 'Select Files', emptyTip1: 'Drag files here ...', emptyTip2: 'Supports multiple file upload/download', emptyTip3: 'Supports pasting images to upload', fileSizeError: 'File size exceeds limit', preview: 'Preview', downloadingStatus: 'Downloading:', downloadingFile: 'Downloading:', downloadComplete: 'Download complete', downloadResult: 'Total {total} files, {success} succeeded, {failed} failed', close: 'Close' } }, // ===================== // 静态icon // ===================== staticIconMap: { download: ` `, delete: ` `, select: ` `, upload: ` `, preview: ` `, more: ` ` }, loadScripts: function (items, callback) { 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 () { var self = this; 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.updateUI(); if (self.options.data && self.options.data.length) { self.setFiles(self.options.data); } }, ); return this; }, // ===================== // 生成唯一ID // ===================== generateUniqueId: function () { return "file_" + Math.random().toString(36).slice(2) + Date.now(); }, // ===================== // 获取文件图标 // ===================== getFileIcon: function (filename) { var ext = filename.split('.').pop().toLowerCase(); var iconMap = { pdf: '', png: '', jpg: '', default: '' }; // 不做处理 if (this.isImageFile(filename)) { return '' } return iconMap[ext] || iconMap.default; }, // ===================== // 渲染 // ===================== render: function () { const html = `
${this.language.selectAll}
${this.language.emptyTip1} ${this.language.emptyTip2} ${this.language.emptyTip3}
Task Running
0 / 0 0%
Total: 0 | Success: 0 | Fail: 0
`; 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'); this.$taskDock = this.$container.find('.task-dock'); this.$uiLock = this.$container.find('.ui-lock'); 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); var self = this; 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(); }); // ========================= // ⭐ 强制定位到 more 按钮 // ========================= var $anchor = $(actionMenuState.anchorEl).closest('.g3-file-action.more'); var rect = $anchor[0].getBoundingClientRect(); var menuWidth = 160; var itemHeight = 36; var menuHeight = actionMenuState.items.length * itemHeight; var viewportW = window.innerWidth; var viewportH = window.innerHeight; // ========================= // ⭐ fixed 模式:不要 scrollY! // ========================= var top = rect.bottom; var left = rect.right - menuWidth; var spaceBottom = viewportH - rect.bottom; var spaceTop = rect.top; var openUp = spaceBottom < menuHeight && spaceTop > menuHeight; if (openUp) { // 向上打开时,菜单顶部应该与 file-item 顶部对齐,而不是 file-action 顶部 var $fileItem = $anchor.closest('.g3-file-item'); var fileItemRect = $fileItem[0].getBoundingClientRect(); top = fileItemRect.top; } // 左侧兜底 if (rect.right < menuWidth) { left = rect.left; } left = Math.max(8, Math.min(left, viewportW - menuWidth - 8)); actionMenu.css({ top: top, left: left }); requestAnimationFrame(function () { actionMenu.addClass('is-open'); }); }, getFileById: function (id) { return this.files.find(function (f) { return f.id === id; }); }, compressImage: async function (file, options = {}) { const { minSize = 300 * 1024, maxSize = 1.2 * 1024 * 1024, // 放宽上限(避免过压) maxWidth = 1920, maxHeight = 1920, } = options; if (!file || !file.type.startsWith("image/")) return file; const originSize = file.size; // ===== 🚨 关键1:小图/中图直接不压 ===== if (originSize <= maxSize) { return file; } const img = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const image = new Image(); image.onload = () => resolve(image); image.onerror = reject; image.src = reader.result; }; reader.onerror = reject; reader.readAsDataURL(file); }); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); // ===== 只对“大图”轻微缩放 ===== let w = img.width; let h = img.height; const scale = Math.min(1, maxWidth / w, maxHeight / h); w = Math.round(w * scale); h = Math.round(h * scale); canvas.width = w; canvas.height = h; ctx.drawImage(img, 0, 0, w, h); const getBlob = (q) => new Promise((resolve) => { canvas.toBlob((b) => resolve(b), "image/jpeg", q); }); // ===== 🚨 关键2:只做“单次轻压” ===== // 不做循环,不做二分 let quality = 0.92; // 只有超大图才稍微降一点 if (originSize > 4 * 1024 * 1024) { quality = 0.88; } const blob = await getBlob(quality); // ===== 🚨 关键3:防止压太小(核心修复)===== if (blob && blob.size < minSize) { // ❗直接回退高质量版本 const safe = await getBlob(0.97); return safe || file; } return blob || file; }, // ===================== // 绑定事件 // ===================== 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.updateUI(); }); // 批量下载 this.$btnDownload.off('click').on('click', function () { var selectedFiles = self.getSelectedFiles(); if (selectedFiles.length === 0) return; self.handleDownload(selectedFiles, { batch: true }); }); // 批量删除 this.$btnDelete.off('click').on('click', function () { var selectedFiles = self.getSelectedFiles(); if (selectedFiles.length === 0) return; self.handleDelete(selectedFiles); }); // 选择文件 this.$btnSelect.off('click').on('click', function () { self.$fileInput.click(); }); // 上传 this.$btnUpload.off('click').on('click', function () { var pendingFiles = self.files.filter(file => file.status === 'pending'); 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; self.handleFileSelect(files); // 重置文件输入框 self.$fileInput.val(''); }); // 点击其他地方关闭菜单 $(document).off('click.g3file_actionmenu').on('click.g3file_actionmenu', function (e) { if (!$(e.target).closest('.g3-file-action.more').length && !$(e.target).closest('.g3-global-menu').length) { self.closeActionMenu(); } }); }, // ===================== // 处理文件选择 // ===================== 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 (self.isImageFile(file.name)) { // 检查是否有外部自定义压缩算法 if (typeof self.options.compressImage === 'function') { try { compressedFile = await self.options.compressImage(file); } catch (e) { console.warn('外部压缩算法执行失败,使用内部压缩:', e); compressedFile = await self.compressImage(file); } } else { // 使用内部压缩算法 compressedFile = await self.compressImage(file); } url = URL.createObjectURL(compressedFile); self.objectUrls.set(self.generateUniqueId(), url); } tempFiles.push({ id: self.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); }, // ===================== // 任务管理 // ===================== addTask: function (task) { const newTask = { id: task.id || this.generateUniqueId(), name: task.name, type: task.type || 'custom', progress: task.progress || 0, status: task.status || 'running' }; this.tasks.push(newTask); this.updateTaskStats(); this.showTaskDock(); this.lockUI(); return newTask.id; }, updateTask: function (taskId, updates) { const task = this.tasks.find(t => t.id === taskId); if (task) { Object.assign(task, updates); this.updateTaskDock(); } }, finishTask: function (taskId, status) { const task = this.tasks.find(t => t.id === taskId); if (task) { task.status = status || 'success'; task.progress = 100; this.updateTaskStats(); this.updateTaskDock(); // 检查是否所有任务都已完成 const allCompleted = this.tasks.every(t => t.status === 'success' || t.status === 'fail'); if (allCompleted) { this.unlockUI(); } } }, updateTaskStats: function () { this.taskStats.total = this.tasks.length; this.taskStats.success = this.tasks.filter(t => t.status === 'success').length; this.taskStats.fail = this.tasks.filter(t => t.status === 'fail').length; }, clearTasks: function () { this.tasks = []; this.taskStats = { total: 0, success: 0, fail: 0 }; this.hideTaskDock(); this.unlockUI(); }, // ===================== // Task Dock UI 管理 // ===================== showTaskDock: function () { if (this.$taskDock) { this.$taskDock.show(); this.bindTaskDockEvents(); } }, hideTaskDock: function () { if (this.$taskDock) { this.$taskDock.hide(); } }, minimizeTaskDock: function () { if (this.$taskDock) { const dockBody = this.$taskDock.find('.dock-body'); if (dockBody.is(':visible')) { dockBody.hide(); } else { dockBody.show(); } } }, updateTaskDock: function () { if (!this.$taskDock || this.tasks.length === 0) return; // 获取当前正在运行的任务,或者最后一个任务 const currentTask = this.tasks.find(t => t.status === 'running') || this.tasks[this.tasks.length - 1]; if (currentTask) { // 计算整体进度 const totalProgress = this.tasks.reduce((sum, task) => sum + task.progress, 0) / this.tasks.length; // 更新 UI this.$taskDock.find('.progress-line .count').text(`${this.taskStats.success + this.taskStats.fail} / ${this.taskStats.total}`); this.$taskDock.find('.progress-line .percent').text(`${Math.round(totalProgress)}%`); this.$taskDock.find('.task-name').text(currentTask.name); this.$taskDock.find('.task-type').text(currentTask.type); this.$taskDock.find('.bar .bar-inner').css('width', `${currentTask.progress}%`); this.$taskDock.find('.task-result').text(`Total: ${this.taskStats.total} | Success: ${this.taskStats.success} | Fail: ${this.taskStats.fail}`); } }, bindTaskDockEvents: function () { const self = this; // 绑定关闭按钮事件 this.$taskDock.find('.dock-actions button[title="close"]').off('click').on('click', function () { self.clearTasks(); }); // 绑定最小化按钮事件 this.$taskDock.find('.dock-actions button[title="minimize"]').off('click').on('click', function () { self.minimizeTaskDock(); }); }, // ===================== // UI Lock 功能 // ===================== lockUI: function () { if (this.$uiLock) { this.$uiLock.show(); } }, unlockUI: function () { if (this.$uiLock) { this.$uiLock.hide(); } }, isUILocked: function () { return this.$uiLock && this.$uiLock.is(':visible'); }, // ===================== // 绑定文件项事件 // ===================== bindFileItemEvents: function () { var self = this; // 预览 this.$fileList.off('click', '.preview') .on('click', '.preview', function (e) { e.preventDefault(); e.stopPropagation(); var id = $(this).closest('.g3-file-item').data('id'); var file = self.files.find(f => f.id === id); if (!file) return; if (typeof self.options.onPreview === 'function') { self.options.onPreview([file]); } }); // 下载 this.$fileList.off('click', '.download') .on('click', '.download', function (e) { e.preventDefault(); e.stopPropagation(); var id = $(this).closest('.g3-file-item').data('id'); var file = self.files.find(f => f.id === id); if (!file) return; self.handleDownload([file], { batch: false }); }); // 复选框 this.$fileList.off('change', '.g3-file-checkbox') .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(); }); // 删除 this.$fileList.off('click', '.g3-file-delete-btn') .on('click', '.g3-file-delete-btn', function (e) { e.preventDefault(); e.stopPropagation(); var id = $(this).closest('.g3-file-item').data('id'); var file = self.files.find(f => f.id === id); if (!file) return; self.handleDelete([file]); }); // 文件项点击 this.$fileList.off('click', '.g3-file-item') .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.files.find(f => f.id === id); if (!file) return; if (typeof self.options.onPreview === 'function') { self.options.onPreview([file]); } }); this.$fileList.off('click', '.more') .on('click', '.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); }); }, // ===================== // 处理下载 // ===================== 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]); } }, // ===================== // 处理删除 // ===================== handleDelete: function (files) { var self = this; if (typeof this.options.onDelete === 'function') { this.options.onDelete(files, function (success) { if (success) { self.internalDelete(files); } }); return; } this.internalDelete(files); }, // ===================== // 内部下载 // ===================== internalDownload: async function (file) { if (!file) { console.warn('下载失败:文件对象无效', file); 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); return; } saveAs(blob, file.name || '未命名文件'); } catch (err) { console.error('文件下载失败:', err); } }, // ===================== // 内部批量下载 // ===================== internalDownloadBatch: async function (files) { if (!files || files.length === 0) return; const zip = new JSZip(); const addedFiles = new Set(); const total = files.length; let failedCount = 0; const concurrency = 3; // 为每个文件创建任务 const taskIds = []; files.forEach(file => { const taskId = this.addTask({ name: file.name, type: 'download', progress: 0, status: 'running' }); taskIds.push(taskId); }); const downloadOne = async (item, index) => { const taskId = taskIds[index]; try { // 更新任务进度 this.updateTask(taskId, { progress: 0, status: 'running' }); 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++; this.finishTask(taskId, 'fail'); return; } const uniqueFileName = this.getUniqueFileName(item.name, addedFiles); zip.file(uniqueFileName, content); addedFiles.add(uniqueFileName); // 标记任务完成 this.finishTask(taskId, 'success'); } catch (err) { console.warn('跳过失败文件:', item.name, err); failedCount++; this.finishTask(taskId, 'fail'); } }; try { // 使用副本,不修改原始数据 const filesCopy = [...files]; const runner = async () => { while (filesCopy.length) { const index = total - filesCopy.length; const item = filesCopy.shift(); await downloadOne(item, index); } }; 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"); const successCount = total - failedCount; console.log(`下载完成:${successCount}/${total},失败:${failedCount}`); // 所有任务完成后,保持 dock 显示一段时间,让用户查看结果 setTimeout(() => { this.clearTasks(); }, 3000); } catch (err) { console.error("批量下载失败:", err); // 失败时清理任务 this.clearTasks(); } }, // ===================== // 内部删除 // ===================== 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(); }, // ===================== // 获取文件Blob // ===================== getFileBlob: async function (url) { if (!url) { throw new Error('无效的文件URL'); } const res = await fetch(url); if (!res.ok) { throw new Error('网络请求失败,状态码:' + res.status); } const buffer = await res.arrayBuffer(); return buffer; }, // ===================== // 获取唯一文件名 // ===================== getUniqueFileName: function (fileName, files) { const originalFileName = fileName; let uniqueFileName = fileName; let counter = 1; while (files.has(uniqueFileName)) { const fileNameWithoutExtension = originalFileName.replace(/\.[^/.]+$/, ""); const extension = originalFileName.slice(originalFileName.lastIndexOf(".")); uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`; counter++; } return uniqueFileName; }, // ===================== // 判断是否为图片文件 // ===================== 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); }, // ===================== // 格式化文件大小 // ===================== 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 previewDisabled = !this.hasPermission(file, 'view'); const downloadDisabled = !this.hasPermission(file, 'download'); const deleteDisabled = !this.hasPermission(file, 'delete'); let content = ""; if (this.isImageFile(file.name)) { content = `
${file.name}
${this.staticIconMap.error || ''}
`; } else { content = this.getFileIcon(file.name); } const pendingBadge = file.status === 'pending' ? `
${this.language.pending}
` : ''; const deleteButton = `
${this.staticIconMap.delete}
`; // 预览状态(使用class标识) const isPreview = this.previewFiles.has(file._id); // 选中状态 const isSelected = this.selectedFiles.has(file._id); return `
${content}
${pendingBadge} ${deleteButton}
${file.name} ${this.formatFileSize(file.size)} ${this.formatDate(file.uploadDate)}
${this.staticIconMap.preview}
${this.staticIconMap.download}
${this.staticIconMap.more}
`; }, closeActionMenu: function () { this.actionMenuState.open = false; this.actionMenuState.fileId = null; this.actionMenuState.anchorEl = null; this.actionMenuState.items = []; this.renderActionMenu(); }, 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(); }, // ===================== // 渲染文件列表 // ===================== 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); this.bindFileItemEvents(); }, // ===================== // 更新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-file-action.download') .toggleClass('is-disabled', !canDownload); $item.find('.g3-file-action.preview') .toggleClass('is-disabled', !canView); $item.find('.g3-file-delete-btn') .toggleClass('is-disabled', !canDelete); }); }, // ===================== // 获取选中文件 // ===================== getSelectedFiles: function () { return this.files.filter(file => this.selectedFiles.has(file.id)); }, // ===================== // 设置文件 // ===================== setFiles: function (files) { var self = this; this.files = files.map(function (file) { return { id: file.id || self.generateUniqueId(), name: file.name, size: file.size, uploadDate: file.uploadDate || new Date().toISOString(), url: file.url || '', file: file.file || null, status: file.status || 'uploaded' }; }); this.selectedFiles.clear(); this.renderFileList(); this.updateUI(); return this; }, // ===================== // 更新状态 // ===================== updateState: function (partialState) { if (partialState.files) { this.setFiles(partialState.files); } else { // 其他状态更新时清理任务 this.clearTasks(); } if (partialState.selectedFiles) { this.selectedFiles = new Set(partialState.selectedFiles); this.updateUI(); } if (partialState.globalPermission || partialState.filePermissionResolver) { this.updatePermissions({ globalPermission: partialState.globalPermission, filePermissionResolver: partialState.filePermissionResolver }); } return this; }, // ===================== // 权限判断(唯一入口) // ===================== hasPermission: function (file, action) { var globalPermission = this.options.globalPermission; var filePermissionResolver = this.options.filePermissionResolver; // 1. 优先判断单文件权限解析函数 if (typeof filePermissionResolver === 'function') { var fileResult = filePermissionResolver({ file: file, action: action }); if (fileResult !== undefined) { return !!fileResult; } } // 2. 判断全局权限 if (typeof globalPermission === 'function') { return !!globalPermission({ file: file, action: action }); } else if (typeof globalPermission === 'object') { return !!globalPermission[action]; } // 默认返回 true return true; }, // ===================== // 更新权限配置 // ===================== 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(); }, // ===================== // 销毁 // ===================== destroy: function () { this.$container.off(); // 清理ObjectURL this.objectUrls.forEach((url) => { try { URL.revokeObjectURL(url); } catch (e) { console.warn('释放ObjectURL失败:', e); } }); this.objectUrls.clear(); // 清理引用 this.files = []; this.selectedFiles.clear(); this.$container.empty(); 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); } } }); return instance || this; }; })(jQuery);