Files
2026-04-28 21:47:04 +08:00

11 KiB
Raw Permalink Blame History

G3File 组件结构优化和缺陷修复计划

一、P0 必须修复的问题

1. 避免整列表重渲染(核心性能问题)

问题:updateFileStatus 调用 renderFileList(),导致整个列表重新渲染 + 重新绑定事件

解决方案:新增 updateFileItem(fileId) 方法,仅更新单个文件项

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() 多任务模型

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 中统一绑定一次

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 实现并发控制

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 方法中统一释放

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) 统一方法

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 会触发预览,容易误触

解决方案:改为双击触发预览

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 锁机制完善

解决方案:在关键操作开始时检查锁状态

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 回调支持

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

.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 规范化

解决方案:建立层级规范

: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 规范化