# G3File 组件结构优化和缺陷修复计划 ## 一、P0 必须修复的问题 ### 1. 避免整列表重渲染(核心性能问题) **问题**:`updateFileStatus` 调用 `renderFileList()`,导致整个列表重新渲染 + 重新绑定事件 **解决方案**:新增 `updateFileItem(fileId)` 方法,仅更新单个文件项 ```javascript updateFileItem: function (fileId) { var file = this.getFileById(fileId); var $item = this.$fileList.find(`.g3-file-item[data-id="${fileId}"]`); if (!file || !$item.length) return; // 更新 checkbox $item.find(".g3-file-checkbox").prop("checked", this.selectedFiles.has(fileId)); // 更新 badge var $badge = $item.find(".g3-status-badge"); if (file.status === "pending") { $badge.attr("class", "g3-status-badge g3-status-pending").text(this.language.pending); } else if (file.status === "uploading") { $badge.attr("class", "g3-status-badge g3-status-uploading").text(this.language.uploading); } else if (file.status === "failed") { $badge.attr("class", "g3-status-badge g3-status-failed").text(this.language.uploadFailed); } else if (file.status === "uploaded") { $badge.attr("class", "g3-status-badge g3-status-uploaded").text(this.language.uploaded); } // 更新权限状态 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); } ``` **修改点**: - `updateFileStatus` 方法中移除 `renderFileList()` 调用,改为 `updateFileItem(fileId)` --- ### 2. 预览逻辑设计缺陷(previewIndex 问题) **问题**:使用 `previewIndex` 作为预览定位,删除/新增文件会导致错位 **解决方案**:改为使用 `currentPreviewFileId` **修改点**: 1. 构造函数中:`this.previewIndex = 0` → `this.currentPreviewFileId = null` 2. 新增方法 `getPreviewFileIndex()` 用于兼容 prev/next 导航 3. `previewPrev()` 和 `previewNext()` 改为基于 `currentPreviewFileId` 计算 4. `updatePreview()` 改为使用 `currentPreviewFileId` 获取文件 5. `setPreviewFile()` 直接设置 `currentPreviewFileId` 6. `setFiles()` 中:`this.previewIndex = 0` → `this.currentPreviewFileId = null` --- ### 3. Task Center 多任务支持 **问题**:`this.currentTask` 只能存在一个任务,上传/下载会互相覆盖 **解决方案**:改为 `this.tasks = new Map()` 多任务模型 ```javascript 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.renderTaskCenter(task.id); this.lockUI(); return task.id; } 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); } 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(); } } ``` **修改点**: - `this.currentTask` → `this.tasks = new Map()` - `renderTaskCenter(taskId)` 和 `updateTaskCenter(taskId)` 接收 taskId 参数 - `closeTask(id)` 从 `this.tasks` 中删除任务 --- ## 二、P1 重要优化 ### 4. 事件绑定优化 **问题**:每次 `renderFileList` 后都调用 `bindFileItemEvents` 重新绑定 **解决方案**:使用事件委托,在 `bindEvents` 中统一绑定一次 ```javascript bindEvents: function () { var self = this; // 文件列表事件委托(只在 init 时绑定一次) this.$fileList .on("click", ".g3-action-preview", function (e) { ... }) .on("click", ".g3-action-download", function (e) { ... }) .on("change", ".g3-file-checkbox", function (e) { ... }) .on("click", ".g3-file-delete-btn", function (e) { ... }) .on("click", ".g3-file-item", function (e) { ... }) .on("click", ".g3-action-more", function (e) { ... }); // 其他事件保持不变... } ``` **修改点**: - `renderFileList()` 中移除 `bindFileItemEvents()` 调用 - `bindFileItemEvents()` 方法可移除或保留(如果外部需要单独调用) --- ### 5. 上传并发控制 **问题**:`handleUpload` 中 forEach 并发无限制 **解决方案**:使用 async pool 实现并发控制 ```javascript handleUpload: function (files) { var self = this; var concurrency = 3; var pendingFiles = files.filter((file) => file.status === "pending"); if (pendingFiles.length === 0) return; var total = pendingFiles.length; var taskId = this.startTask({ title: this.language.batchUpload || "批量上传", total: total }); var successCount = 0; var failCount = 0; var index = 0; var uploadOne = async function () { while (index < pendingFiles.length) { var file = pendingFiles[index++]; self.updateFileStatus(file._id, "uploading"); try { var uploadedFile = await self.internalUpload(file); successCount++; self.updateFileStatus(file._id, "uploaded", uploadedFile); if (typeof self.options.onUploadSuccess === "function") { self.options.onUploadSuccess(uploadedFile); } } catch (error) { failCount++; self.updateFileStatus(file._id, "failed"); if (typeof self.options.onUploadError === "function") { self.options.onUploadError(file, error); } } self.updateTask({ id: taskId, current: successCount + failCount, currentItem: file.name }); } }; Promise.all(Array.from({ length: concurrency }, uploadOne)).then(function () { self.finishTask({ id: taskId, total: total, success: successCount, failed: failCount }); if (typeof self.options.onUploadComplete === "function") { self.options.onUploadComplete(successCount, failCount); } }); } ``` **修改点**: - `handleUpload` 方法重写,实现并发控制 --- ### 6. ObjectURL 生命周期管理 **问题**:组件销毁时未释放 ObjectURL **解决方案**:`destroy` 方法中统一释放 ```javascript destroy: function () { // 清理事件 this.$container.off(); $(document).off(".g3file"); // 清理 ObjectURL this.objectUrls.forEach(function (url) { try { URL.revokeObjectURL(url); } catch (e) { console.warn("释放ObjectURL失败:", e); } }); this.objectUrls.clear(); // 清理文件列表 DOM this.$fileList.empty(); // 销毁 UI 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.files = []; this.selectedFiles.clear(); this.tasks.clear(); this.currentPreviewFileId = null; console.log("G3File 组件已销毁"); } ``` --- ### 7. 权限系统统一入口 **问题**:globalPermission 和 filePermissionResolver 分散使用 **解决方案**:新增 `resolvePermission(file, action)` 统一方法 ```javascript resolvePermission: function (file, action) { var globalPermission = this.options.globalPermission; var filePermissionResolver = this.options.filePermissionResolver; if (typeof filePermissionResolver === "function") { var result = filePermissionResolver({ file: file, action: action }); if (result !== undefined) return !!result; } if (typeof globalPermission === "function") { return !!globalPermission({ file: file, action: action }); } else if (typeof globalPermission === "object") { return !!globalPermission[action]; } return true; } ``` **修改点**: - `hasPermission` 重命名为 `resolvePermission` 或新增方法,原方法保留为别名 - 所有调用 `hasPermission` 的地方改为调用 `resolvePermission` --- ## 三、P2 UI/交互修复 ### 8. 点击文件项行为优化 **问题**:点击整个 file-item 会触发预览,容易误触 **解决方案**:改为双击触发预览 ```javascript this.$fileList.on("dblclick", ".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; self.setPreviewFile(id); if (typeof self.options.onPreview === "function") { self.options.onPreview([file]); } }); ``` **修改点**: - `bindEvents` 中的 `.g3-file-item` 点击事件改为双击 --- ### 9. UI 锁机制完善 **解决方案**:在关键操作开始时检查锁状态 ```javascript handleUpload: function (files) { if (this.uiLocked) { console.warn("UI is locked, please wait..."); return; } // ... 原有逻辑 } internalDelete: function (files) { if (this.uiLocked) { console.warn("UI is locked, please wait..."); return; } // ... 原有逻辑 } ``` --- ### 10. 批量删除安全控制 **解决方案**:增加 confirm 回调支持 ```javascript internalDelete: function (files) { var self = this; if (typeof this.options.onBeforeDelete === "function") { var result = this.options.onBeforeDelete(files); if (result === false) return; if (typeof result === "function") { // 异步确认 result(function (confirmed) { if (confirmed) self.doDelete(files); }); return; } } self.doDelete(files); } doDelete: function (files) { // 原 internalDelete 逻辑 } ``` **修改点**: - 新增 `onBeforeDelete` 回调选项 - 重构删除逻辑 --- ## 四、CSS 优化 ### 11. 文件列表布局优化 **问题**:使用 calc 写死宽度 **解决方案**:改为 flex: calc(50% - 26px) 或 grid ```css .g3-main { display: flex; flex-wrap: wrap; align-content: flex-start; } .g3-file-item { flex: 0 0 calc(50% - 20px); max-width: calc(50% - 20px); } ``` --- ### 12. z-index 规范化 **解决方案**:建立层级规范 ```css :root { --g3-z-dropdown: 100; --g3-z-modal: 200; --g3-z-toast: 300; --g3-z-task: 400; --g3-z-lock: 500; } ``` --- ## 五、实施步骤 1. **P0-1**: 新增 `updateFileItem()` 方法,修改 `updateFileStatus()` 2. **P0-2**: 替换 `previewIndex` 为 `currentPreviewFileId` 3. **P0-3**: 重构 Task Center 为多任务模型 4. **P1-4**: 事件委托优化 5. **P1-5**: 上传并发控制 6. **P1-6**: 完善 `destroy()` 方法 7. **P1-7**: 新增 `resolvePermission()` 方法 8. **P2-8**: 双击预览 9. **P2-9**: UI 锁检查 10. **P2-10**: 删除确认回调 11. **CSS**: 布局优化和 z-index 规范化