u
This commit is contained in:
1 parent
2a00dc7ef4
commit
c6d55b0f46
4 files changed
+801
-339
No files matched your search
@@ -0,0 +1,45 @@
|
||||
# Task 关闭按钮修复计划
|
||||
|
||||
## 问题分析
|
||||
|
||||
根据代码审查,`g3-task-close` 点击无反应的核心问题在于:
|
||||
|
||||
1. **事件绑定方式错误**:使用直接绑定而非事件委托
|
||||
2. **taskId 未绑定到 DOM**:无法通过 DOM 获取当前任务 ID
|
||||
3. **closeTask 方法不完善**:只删除数据,未删除对应的 DOM 元素
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 1. 修改 renderTaskCenter 方法
|
||||
- 在 DOM 元素上添加 `data-task-id` 属性
|
||||
- 移除直接的事件绑定
|
||||
|
||||
### 2. 修改 closeTask 方法
|
||||
- 同时删除数据和对应的 DOM 元素
|
||||
|
||||
### 3. 添加事件委托(在初始化时绑定)
|
||||
- 使用事件委托绑定 `.g3-task-close` 点击事件
|
||||
|
||||
### 4. 检查 pointer-events 样式
|
||||
- 确保 `.g3-task-close` 按钮可点击
|
||||
|
||||
## 修改文件
|
||||
|
||||
- `d:\workspace\code\file-preview\g3-file\index.js`
|
||||
|
||||
## 修改步骤
|
||||
|
||||
1. 在 `renderTaskCenter` 的 HTML 模板中添加 `data-task-id`
|
||||
2. 移除 `renderTaskCenter` 中的直接事件绑定
|
||||
3. 修改 `closeTask` 方法,添加 DOM 删除逻辑
|
||||
4. 在初始化时添加事件委托绑定
|
||||
|
||||
## 风险评估
|
||||
|
||||
- 低风险:修改局部代码,不影响其他功能
|
||||
- 需确保事件委托只绑定一次
|
||||
|
||||
## 验证方式
|
||||
|
||||
1. 点击关闭按钮,检查控制台是否输出正确的 taskId
|
||||
2. 确认任务数据和 UI 都被正确移除
|
||||
@@ -0,0 +1,425 @@
|
||||
# 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 规范化
|
||||
@@ -5,6 +5,11 @@
|
||||
overflow: hidden;
|
||||
border: 1px solid #eef0f2;
|
||||
border-radius: 8px;
|
||||
--g3-z-dropdown: 100;
|
||||
--g3-z-modal: 200;
|
||||
--g3-z-toast: 300;
|
||||
--g3-z-task: 400;
|
||||
--g3-z-lock: 500;
|
||||
}
|
||||
|
||||
.g3-file-container.drag-over {
|
||||
@@ -32,15 +37,13 @@
|
||||
.g3-main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 4px 0px;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.g3-main > * {
|
||||
margin: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.g3-empty-tip {
|
||||
@@ -143,13 +146,15 @@
|
||||
}
|
||||
|
||||
.g3-file-item {
|
||||
width: calc(50% - 26px);
|
||||
flex: none;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: 1px solid #eef0f2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.g3-file-item:hover {
|
||||
@@ -379,7 +384,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
z-index: var(--g3-z-dropdown);
|
||||
font-size: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
transition: all 0.2s ease;
|
||||
@@ -537,7 +542,7 @@
|
||||
|
||||
.g3-global-menu {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
z-index: var(--g3-z-dropdown);
|
||||
width: 160px;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
@@ -578,7 +583,7 @@
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
z-index: var(--g3-z-toast);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
@@ -734,7 +739,7 @@
|
||||
border: 1px solid #eef0f2;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 9999;
|
||||
z-index: var(--g3-z-task);
|
||||
display: none;
|
||||
animation: taskCenterSlideIn 0.3s ease-out;
|
||||
}
|
||||
@@ -857,23 +862,6 @@
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.g3-complete-button {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: #165dff;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.g3-complete-button:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.g3-ui-lock {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -881,7 +869,7 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
z-index: 9998;
|
||||
z-index: var(--g3-z-lock);
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
+313
-309
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user