diff --git a/code/container-ocr/ocr-vue/test.html b/code/container-ocr/ocr-vue/test.html index fdf75aaa..89439639 100644 --- a/code/container-ocr/ocr-vue/test.html +++ b/code/container-ocr/ocr-vue/test.html @@ -1,159 +1,376 @@ - - - 多空间 App 原型 - + +附件组件(最终完整版) + + - + -
+
- -
-
首页
+
分类区域(可扩展)
- +
+
- -
- - -
-
- - -
-
任务
-
聊天
-
文件
-
报表
-
- - -
-
最近内容
-
任务:优化UI设计
-
消息:项目讨论
-
-
- - - - - - - +
+
暂无预览
+
请选择文件
- -
- - - -
- -
- - - diff --git a/code/file-preview/.trae/documents/task_dock_refactor_plan.md b/code/file-preview/.trae/documents/task_dock_refactor_plan.md new file mode 100644 index 00000000..74651d14 --- /dev/null +++ b/code/file-preview/.trae/documents/task_dock_refactor_plan.md @@ -0,0 +1,272 @@ +# Task Dock 重构计划 + +## 1. 项目分析 + +### 1.1 当前代码结构 + +* **index.js**: 包含 G3File 构造函数和所有核心逻辑 + +* **index.css**: 包含所有样式定义 + +* **libs/**: 包含 FileSaver 和 JSZip 库 + +### 1.2 当前下载进度条实现 + +* 位于 `g3-file-container` 内部 + +* 固定在顶部的 `download-progress-bar` 元素 + +* 仅支持下载任务 + +* 显示简单的进度信息 + +## 2. 重构目标 + +### 2.1 核心目标 + +1. **移除 download-progress-bar** +2. **新增全局 task dock**(右下角浮层) +3. **支持所有异步任务类型**(不限定 download/upload) + +### 2.2 功能要求 + +1. **全局任务中心**:fixed 右下角浮层 +2. **通用任务模型**:id, name, type, progress, status +3. **批量任务支持**:多个任务同时运行,批量更新进度 +4. **结果汇总**:总任务数、成功数、失败数 +5. **UI Lock**:任务运行时禁止页面其他操作 +6. **交互要求**:close,实时进度更新 + +## 3. 实现计划 + +### 3.1 文件修改 + +#### 3.1.1 index.js 修改 + +1. **移除 download-progress-bar 相关代码**: + + * 删除 `showDownloadProgress` 方法 + + * 删除 `updateDownloadProgress` 方法 + + * 删除 `completeDownloadProgress` 方法 + + * 删除 `$downloadProgressBar` 变量 + +2. **新增 Task 管理功能**: + + * 新增 `tasks` 数组存储任务列表 + + * 新增 `taskStats` 对象存储统计信息 + + * 新增 `addTask` 方法 + + * 新增 `updateTask` 方法 + + * 新增 `finishTask` 方法 + + * 新增 `updateTaskStats` 方法 + + * 新增 `clearTasks` 方法 + +3. **新增 Task Dock 管理**: + + * 新增 `renderTaskDock` 方法 + + * 新增 `updateTaskDock` 方法 + + * 新增 `showTaskDock` 方法 + + * 新增 `hideTaskDock` 方法 + + * 新增 `minimizeTaskDock` 方法 + +4. **新增 UI Lock 功能**: + + * 新增 `lockUI` 方法 + + * 新增 `unlockUI` 方法 + + * 新增 `isUILocked` 方法 + +5. **修改现有方法**: + + * 修改 `internalDownloadBatch` 方法使用新的任务管理 + + * 修改 `handleDownload` 方法支持任务管理 + +#### 3.1.2 index.css 修改 + +1. **移除 download-progress-bar 样式** +2. **新增 task-dock 相关样式**: + + * `.task-dock`:基础容器样式 + + * `.dock-header`:头部样式 + + * `.dock-body`:身体样式 + + * `.progress-line`:进度行样式 + + * `.task-name`:任务名称样式 + + * `.task-type`:任务类型样式 + + * `.bar`:进度条样式 + + * `.bar-inner`:进度条内部样式 + + * `.task-result`:结果汇总样式 + + * `.ui-lock`:UI 锁定遮罩样式 + +### 3.2 实现步骤 + +1. **步骤 1:修改 CSS 样式** + + * 移除 `download-progress-bar` 相关样式 + + * 添加 `task-dock` 相关样式 + + * 添加 `ui-lock` 遮罩样式 + +2. **步骤 2:修改 HTML 结构** + + * 从 `render` 方法中移除 `download-progress-bar` 元素 + + * 添加 `task-dock` 元素到页面 + +3. **步骤 3:实现 Task 管理功能** + + * 添加任务管理相关变量 + + * 实现任务管理方法 + +4. **步骤 4:实现 Task Dock UI 管理** + + * 实现 dock 渲染和更新方法 + + * 实现 dock 显示/隐藏/最小化方法 + +5. **步骤 5:实现 UI Lock 功能** + + * 实现 UI 锁定和解锁方法 + +6. **步骤 6:修改现有方法** + + * 修改 `internalDownloadBatch` 方法使用新的任务管理 + + * 确保所有异步任务都通过任务中心管理 + +7. **步骤 7:测试验证** + + * 测试批量下载功能 + + * 测试任务状态更新 + + * 测试 UI Lock 功能 + + * 测试交互功能(close、minimize) + +## 4. 技术实现细节 + +### 4.1 任务模型 + +```javascript +{ + id: "task_123", + name: "report.pdf", + type: "download", // download / upload / custom + progress: 0, // 0-100 + status: "running" // running / success / fail / paused +} +``` + +### 4.2 统计模型 + +```javascript +{ + total: 10, + success: 7, + fail: 3 +} +``` + +### 4.3 DOM 结构 + +```html +
+
+ Task Running +
+ + +
+
+
+
+ 3 / 10 + 30% +
+
report.pdf
+
download / upload / custom
+
+
+
+
+ Total: xxx | Success: xxx | Fail: xxx +
+
+
+``` + +### 4.4 样式设计 + +* **位置**:fixed 定位在右下角 + +* **大小**:固定宽度,高度自适应 + +* **颜色**:使用与现有设计一致的配色 + +* **动画**:平滑的显示/隐藏动画 + +* **响应式**:适配不同屏幕尺寸 + +## 5. 风险评估 + +### 5.1 潜在风险 + +1. **兼容性问题**:确保新功能在所有浏览器中正常工作 +2. **性能问题**:确保任务管理不会影响页面性能 +3. **用户体验**:确保新的任务中心比原有的进度条更直观易用 + +### 5.2 风险缓解措施 + +1. **兼容性测试**:在主流浏览器中测试功能 +2. **性能优化**:使用防抖和节流技术减少 DOM 更新 +3. **用户测试**:确保新的任务中心操作流畅直观 + +## 6. 预期效果 + +1. **功能增强**:支持多种任务类型,提供更详细的任务信息 +2. **用户体验**:右下角浮层不干扰主界面操作 +3. **可扩展性**:易于添加新的任务类型和功能 +4. **代码质量**:UI 与逻辑分离,代码结构清晰 + +## 7. 时间估计 + +* **CSS 样式修改**:30 分钟 + +* **HTML 结构修改**:15 分钟 + +* **Task 管理功能实现**:60 分钟 + +* **Task Dock UI 实现**:45 分钟 + +* **UI Lock 功能实现**:30 分钟 + +* **现有方法修改**:30 分钟 + +* **测试验证**:45 分钟 + +**总时间**:约 255 分钟(4.25 小时) diff --git a/code/file-preview/custom-file/icons/Choose.svg b/code/file-preview/custom-file/icons/Choose.svg deleted file mode 100644 index 07c54d2b..00000000 --- a/code/file-preview/custom-file/icons/Choose.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/file-preview/custom-file/icons/Delete.svg b/code/file-preview/custom-file/icons/Delete.svg deleted file mode 100644 index b72f6eef..00000000 --- a/code/file-preview/custom-file/icons/Delete.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/file-preview/custom-file/icons/Download.svg b/code/file-preview/custom-file/icons/Download.svg deleted file mode 100644 index 1090d9df..00000000 --- a/code/file-preview/custom-file/icons/Download.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/file-preview/custom-file/icons/Upload.svg b/code/file-preview/custom-file/icons/Upload.svg deleted file mode 100644 index 4d792887..00000000 --- a/code/file-preview/custom-file/icons/Upload.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/file-preview/custom-file/index.css b/code/file-preview/custom-file/index.css index 3c9d611f..bb0b6c7d 100644 --- a/code/file-preview/custom-file/index.css +++ b/code/file-preview/custom-file/index.css @@ -9,6 +9,7 @@ overflow: hidden; } +/* 头部样式 */ .g3-file-container .header { display: flex; align-items: center; @@ -19,23 +20,30 @@ border-bottom: 1px solid #eef0f2; } +/* 主要内容区域 */ .g3-file-container .main { flex: 1; min-height: 0; + padding: 4px 0px; overflow: auto; display: flex; flex-wrap: wrap; - align-items: flex-start; + align-content: flex-start; + + border-left: 1px solid #eef0f2; + border-right: 1px solid #eef0f2; + /* justify-content: center; */ } .g3-file-container .main > * { - margin: 7px; + margin: 10px; } +/* 空状态提示 */ .g3-file-container .empty-tip { flex: 1; min-height: 0; @@ -47,6 +55,7 @@ color: #999; } +/* 底部样式 */ .g3-file-container .footer { display: flex; align-items: center; @@ -55,7 +64,7 @@ padding: 10px 14px; background: #fafbfc; - border-top: 1px solid #eef0f2; + border-top: 1px solid #eee; } .g3-file-container .header, @@ -93,6 +102,7 @@ .g3-file-container .check-all span { margin-left: 4px; + color: #333; } /* 按钮 */ @@ -140,50 +150,81 @@ background: #ff4d4f; } -.g3-file-container .file-item { - width: calc(50% - 20px); +/* 文件项样式 */ +.g3-file-container .g3-file-item { + width: calc(50% - 26px); position: relative; - overflow: hidden; cursor: pointer; - transition: all 0.18s ease; border: 1px solid #eef0f2; - border-radius: 6px; + border-radius: 8px; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); } -.g3-file-container .file-item:hover { - border-color: #d6e4ff; - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); - transform: translateY(-2px); +.g3-file-container .g3-file-item:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); } -.file-item .file-preview { - height: 120px; +/* 文件预览 */ +.g3-file-item .g3-file-preview { + height: 100px; width: 100%; display: flex; align-items: center; justify-content: center; + background-color: #fafbfc; +} + +.g3-file-item .g3-file-preview .g3-file-img { + width: 100%; + height: 100%; +} + +.g3-file-item .g3-file-preview svg { + height: 86px; + width: 100%; +} + +/* 图片容器样式 */ +.g3-file-item .g3-file-preview .g3-img-container { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; } -.file-item .file-preview svg, -.file-item .file-preview img { +/* 图片错误状态 */ +.g3-file-item .g3-file-preview .g3-img-container.is-error .g3-img-error { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: #f0f2f5; +} + +.g3-file-item .g3-file-preview .g3-img-container .g3-img-error { + display: none; + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; } -.file-item .file-checkbox { - position: absolute; - top: 10px; - left: 10px; - width: 16px; - height: 16px; +.g3-file-item .g3-file-preview .g3-img-container .g3-img-error svg { + width: 48px; + height: 48px; + color: #999; } -.file-item .file-info { - padding: 10px 10px 12px; +/* 文件信息 */ +.g3-file-item .g3-file-info { + padding: 8px; font-size: 12px; color: #999; line-height: 1.4; @@ -196,125 +237,129 @@ box-sizing: border-box; } -.file-item .file-info span { +.g3-file-item .g3-file-info span { display: block; width: 100%; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + font-size: 12px; + color: #888; } -.file-item .file-info .file-name { +.g3-file-item .g3-file-info .g3-file-name { font-size: 13px; - color: #1f2937; - font-weight: 600; + color: #000000; } -.file-item .file-actions { +/* 文件操作按钮 */ +.g3-file-item .g3-file-actions { display: flex; align-items: center; justify-content: flex-end; - margin-bottom: 8px; - margin-right: 8px; + + overflow: hidden; + border-top: 1px solid #f0f2f5; } -.file-item .file-action { - width: 24px; - height: 24px; - +.g3-file-item .g3-file-action { + padding: 8px 2px; + flex: 1; display: flex; align-items: center; justify-content: center; - - border-radius: 6px; - - /* 👇 稍微更明显 */ - background: rgba(0, 0, 0, 0.06); - - margin-left: 6px; - - color: rgba(0, 0, 0, 0.6); + gap: 4px; cursor: pointer; - transition: all 0.2s ease; + font-size: 12px; + border-right: 1px solid #f0f2f5; } -/* hover:明显增强 */ -.file-item .file-action:hover { +.g3-file-item .g3-file-action:hover { + background-color: #f5f6f8; + color: #333; +} + +.g3-file-item .g3-file-action:active { background: rgba(22, 93, 255, 0.12); - color: #165dff; } -/* active:点击反馈 */ -.file-item .file-action:active { - background: rgba(22, 93, 255, 0.18); - transform: scale(0.96); -} - -.file-item .file-action.is-disabled { +.g3-file-item .g3-file-action.is-disabled { opacity: 0.4; pointer-events: none; cursor: not-allowed; } -.g3-download-modal { +.g3-file-item .g3-file-action:last-child { + border-right: none; +} + +.g3-file-item .g3-file-action svg { + width: 20px; + height: 20px; + color: #666; +} + +/* 复选框样式 */ +.g3-file-item .g3-file-checkbox { position: absolute; - inset: 0; - z-index: 9999; + top: 8px; + left: 8px; + width: 16px; + height: 16px; + cursor: pointer; + opacity: 0; + z-index: 10; } -.g3-download-mask { +/* 自定义复选框样式 */ +.g3-file-item .g3-file-checkbox + .g3-checkbox-label { position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.4); + top: 8px; + left: 8px; + width: 16px; + height: 16px; + border: 1px solid #d9d9d9; + border-radius: 2px; + background: white; + cursor: pointer; + transition: all 0.2s ease; + z-index: 5; } -.g3-download-box { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: #fff; - padding: 20px; - border-radius: 10px; - width: 260px; - text-align: center; -} - -.g3-download-bar { - height: 6px; - background: #eee; - border-radius: 4px; - margin-top: 10px; - overflow: hidden; -} - -.g3-download-bar-inner { - height: 100%; - width: 0%; - background: #4caf50; - transition: width 0.2s; -} - -/* 文件项选中样式 */ -.g3-file-container .file-item.file-item-selected { +.g3-file-item .g3-file-checkbox:checked + .g3-checkbox-label { + background: #165dff; border-color: #165dff; - box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2); - background: rgba(22, 93, 255, 0.05); } -.g3-file-container .file-item.file-item-selected:hover { +.g3-file-item .g3-file-checkbox:checked + .g3-checkbox-label::after { + content: ""; + position: absolute; + top: 2px; + left: 5px; + width: 4px; + height: 8px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.g3-file-item .g3-file-checkbox:hover + .g3-checkbox-label { border-color: #165dff; - box-shadow: 0 6px 18px rgba(22, 93, 255, 0.2); - transform: translateY(-2px); + box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1); +} + +.g3-file-item .g3-file-checkbox:disabled + .g3-checkbox-label { + opacity: 0.4; + cursor: not-allowed; } /* 未上传标记 */ -.file-item .file-pending-badge { +.g3-file-item .g3-file-pending-badge { position: absolute; - top: 10px; - right: 10px; + top: 6px; + right: 6px; background: #ff9800; color: white; font-size: 10px; @@ -322,3 +367,203 @@ border-radius: 10px; z-index: 1; } + +/* 浮动删除按钮 */ +.g3-file-container .g3-file-delete-btn { + position: absolute; + top: -8px; + right: -8px; + width: 20px; + height: 20px; + border-radius: 50%; + background: #ebebeb; + color: #666; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 100; + font-size: 12px; + border: 1px solid #e8e8e8; + + transition: all 0.2s ease; +} + +.g3-file-container .g3-file-delete-btn:hover { + background: #ff4d4f; + color: white; +} + +.g3-file-container .g3-file-item .g3-file-delete-btn.is-disabled { + opacity: 0.4; + pointer-events: none; + cursor: not-allowed; +} + +/* 全局菜单 */ +.g3-global-menu { + position: absolute; + z-index: 9999; + + width: 160px; + background: #fff; + border: 1px solid #eee; + border-radius: 8px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); + position: fixed; /* ⭐必须,不然一定会错位 */ + + visibility: hidden; + pointer-events: none; +} + +.g3-global-menu.is-open { + visibility: visible; + pointer-events: auto; +} + +/* 更多菜单项样式 */ +.g3-global-menu .g3-action-menu-item { + padding: 8px 16px; + font-size: 14px; + color: #333; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.g3-global-menu .g3-action-menu-item:hover { + background-color: #f5f5f5; +} + +.g3-global-menu .g3-action-menu-item:active { + background-color: #e8e8e8; +} + +/* Task Dock 样式 */ +.task-dock { + position: fixed; + bottom: 20px; + right: 20px; + width: 320px; + background: #ffffff; + border: 1px solid #eef0f2; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 9999; + display: none; + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + transform: translateX(100%) translateY(100%); + opacity: 0; + } + to { + transform: translateX(0) translateY(0); + opacity: 1; + } +} + +.task-dock .dock-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: #fafbfc; + border-bottom: 1px solid #eef0f2; + border-radius: 8px 8px 0 0; +} + +.task-dock .dock-header .title { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.task-dock .dock-actions { + display: flex; + gap: 8px; +} + +.task-dock .dock-actions button { + width: 24px; + height: 24px; + border: none; + background: transparent; + color: #666; + font-size: 16px; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.task-dock .dock-actions button:hover { + background: #f0f2f5; + color: #333; +} + +.task-dock .dock-body { + padding: 16px; +} + +.task-dock .progress-line { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + font-size: 14px; + color: #666; +} + +.task-dock .task-name { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.task-dock .task-type { + font-size: 12px; + color: #999; + margin-bottom: 12px; +} + +.task-dock .bar { + width: 100%; + height: 6px; + background: #f0f2f5; + border-radius: 3px; + overflow: hidden; + margin-bottom: 12px; +} + +.task-dock .bar .bar-inner { + height: 100%; + background: #165dff; + border-radius: 3px; + transition: width 0.3s ease; +} + +.task-dock .task-result { + font-size: 12px; + color: #666; + text-align: right; +} + +/* UI Lock 样式 */ +.ui-lock { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.05); + z-index: 9998; + display: none; +} diff --git a/code/file-preview/custom-file/index.js b/code/file-preview/custom-file/index.js index c8dc4cb0..c92133aa 100644 --- a/code/file-preview/custom-file/index.js +++ b/code/file-preview/custom-file/index.js @@ -1,12 +1,151 @@ (function ($) { 'use strict'; - var G3File = { - files: [], - selectedFiles: new Set(), + + // ===================== + // 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 self = this; var remaining = items.length; function done() { @@ -25,7 +164,7 @@ // 防重复加载 var key = item.key; - if (window[key + '_loading']) { + if (window[key + "_loading"]) { var timer = setInterval(function () { if (item.check && item.check()) { clearInterval(timer); @@ -35,19 +174,19 @@ return; } - window[key + '_loading'] = true; + window[key + "_loading"] = true; - var script = document.createElement('script'); + var script = document.createElement("script"); script.src = item.src; script.onload = function () { - window[key + '_loading'] = false; + window[key + "_loading"] = false; done(); }; script.onerror = function () { - window[key + '_loading'] = false; - console.error(key + ' load failed'); + window[key + "_loading"] = false; + console.error(key + " load failed"); done(); }; @@ -55,137 +194,44 @@ }); }, + // ===================== // 初始化 // ===================== - init: function (containerId, options) { + init: function () { var self = this; - this.$container = $('#' + containerId); - this.options = $.extend({}, this.defaults, options); + 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(); - this.language = this.languages[this.options.language] || this.languages['zh-CN']; - this.files = this.options.data || []; - - this.loadScripts([ - { - key: 'fileSaver', - src: '/custom-file/libs/FileSaver.min.js', - check: function () { - return typeof window.saveAs === 'function'; + if (self.options.data && self.options.data.length) { + self.setFiles(self.options.data); } }, - { - key: 'jsZip', - src: '/custom-file/libs/jszip.min.js', - check: function () { - return typeof window.JSZip === 'function'; - } - } - ], function () { - self.render(); - self.bindEvents(); - self.updatePermissions(self.options.permissions); - - if (self.options.data?.length) { - self.loadFiles(self.options.data); - } - }); + ); return this; }, - // ===================== - // 默认配置 - // ===================== - defaults: { - data: [], - onDelete: null, - onDownload: null, - onSelect: null, - onUpload: null, - language: 'zh-CN', - maxFileSize: 10 * 1024 * 1024, // 默认最大10MB - permissions: { - select: true, - upload: true, - download: true, - delete: true, - view: true - } - }, - - // ===================== - // 多语言 - // ===================== - languages: { - 'zh-CN': { - selectAll: '全选', - download: '批量下载', - delete: '删除', - upload: '上传', - select: '选择', - confirmDelete: '确定要删除选中的 {count} 个文件吗?', - downloading: '准备下载 {count} 个文件', - deleting: '删除中...', - name: '名称', - size: '大小', - date: '日期', - pending: '未上传', - selectFiles: '选择文件', - emptyTip1: '拖拽文件到这里 …', - emptyTip2: '支持多文件同时上传/下载', - emptyTip3: '支持粘贴图片上传', - fileSizeError: '文件大小超过限制' - }, - 'en-US': { - selectAll: 'Select All', - download: 'Batch Download', - delete: 'Delete', - upload: 'Upload', - select: 'Select', - confirmDelete: 'Are you sure to delete {count} selected files?', - downloading: 'Preparing to download {count} files', - deleting: 'Deleting...', - name: 'Name', - size: 'Size', - date: 'Date', - pending: 'Pending', - selectFiles: 'Select Files', - fileSizeError: 'File size exceeds limit' - } - }, - - // ===================== - // 静态icon合计 - // ===================== - staticIconMap: { - download: ` - - `, - delete: ` - - `, - select: ` - - `, - upload: ` - - `, - more: ` - - `, - pdf: ` - - `, - video: ` - - `, - default_file: ` - - ` - }, - // ===================== // 生成唯一ID // ===================== @@ -198,16 +244,19 @@ // ===================== getFileIcon: function (filename) { var ext = filename.split('.').pop().toLowerCase(); - return this.staticIconMap[ext] || this.staticIconMap['default_file']; - }, + var iconMap = { + pdf: '', + png: '', + jpg: '', + default: '' + }; - // ===================== - // 更新语言 - // ===================== - updateLanguage: function (language) { - this.language = this.languages[language] || this.languages['zh-CN']; - this.render(); - return this; + // 不做处理 + if (this.isImageFile(filename)) { + return '' + } + + return iconMap[ext] || iconMap.default; }, // ===================== @@ -223,7 +272,7 @@
+
+
+
+ Task Running +
+ + +
+
+
+
+ 0 / 0 + 0% +
+
+
+
+
+
+
+ Total: 0 | Success: 0 | Fail: 0 +
+
+
+
`; this.$container.html(html); @@ -268,14 +342,194 @@ 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'); @@ -283,140 +537,613 @@ if (checked) { self.files.forEach(function (file) { - self.selectedFiles.add(file._id); + self.selectedFiles.add(file.id); }); } - self.$fileList.find('.file-checkbox').prop('checked', checked); - - self.updateToolbarState(); + self.updateUI(); }); // 批量下载 this.$btnDownload.off('click').on('click', function () { - var idSet = self.selectedFiles || new Set(); + var selectedFiles = self.getSelectedFiles(); + if (selectedFiles.length === 0) return; - var files = self.files.filter(function (item) { - return idSet.has(item._id); - }); - - if (typeof self.options?.onDownloadBatch === 'function') { - self.options.onDownloadBatch(files); - return; - } - - self.internalDownloadBatch(files); + self.handleDownload(selectedFiles, { batch: true }); }); // 批量删除 this.$btnDelete.off('click').on('click', function () { - var idSet = self.selectedFiles || new Set(); + var selectedFiles = self.getSelectedFiles(); + if (selectedFiles.length === 0) return; - if (idSet.size === 0) return; - - var deleteFiles = self.files.filter(function (item) { - return idSet.has(item._id); - }); - - // ===================== - // 1. 优先走外部回调(如果你要接后端) - // ===================== - if (typeof self.options?.onDeleteBatch === 'function') { - self.options.onDeleteBatch(deleteFiles, function (success) { - if (!success) return; - - // 成功后再清理本地 - self.internalDeleteBatch(deleteFiles); - }); - return; - } - - // ===================== - // 2. 没有外部逻辑 -> 直接本地删除 - // ===================== - self.internalDeleteBatch(deleteFiles); + self.handleDelete(selectedFiles); }); - // ===================== - // 选择按钮点击事件(选择本地文件) - // ===================== + // 选择文件 this.$btnSelect.off('click').on('click', function () { self.$fileInput.click(); }); - // ===================== - // 上传按钮点击事件(交给外部处理) - // ===================== + // 上传 this.$btnUpload.off('click').on('click', function () { - // 获取所有未上传的文件 - const pendingFiles = self.files.filter(file => !file.uploaded); + 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; - var maxSize = self.options.maxFileSize || 10 * 1024 * 1024; - var validFiles = []; - var invalidFiles = []; + self.handleFileSelect(files); - for (var i = 0; i < files.length; i++) { - var file = files[i]; - if (file.size > maxSize) { - invalidFiles.push(file); - } else { - validFiles.push(file); - } + // 重置文件输入框 + 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(); } + }); - if (invalidFiles.length > 0) { - var errorMsg = self.language.fileSizeError || '文件大小超过限制'; - alert(errorMsg + ': ' + invalidFiles.map(f => f.name).join(', ')); + }, + + // ===================== + // 处理文件选择 + // ===================== + 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 (validFiles.length > 0) { - // 在本地临时插入文件 - var tempFiles = validFiles.map(function (file) { - // 为图片文件创建本地临时连接 - let url = ''; - if (self.isImageFile(file.name)) { - url = URL.createObjectURL(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); } - return { - _id: self.generateUniqueId(), - name: file.name, - size: file.size, - uploadDate: new Date().toISOString(), - uploaded: false, // 标记为未上传 - url: url, // 图片文件使用本地临时连接,其他文件为空 - file: file // 保存原始文件对象 - }; - }); - - // 添加到文件列表 - self.files = self.files.concat(tempFiles); - self.renderFileList(); - self.updateToolbarState(); - - if (typeof self.options.onSelect === 'function') { - self.options.onSelect(validFiles); + 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.$fileInput.val(''); + 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; @@ -427,86 +1154,6 @@ return imageExts.has(ext); }, - // ===================== - // 更新工具栏状态 - // ===================== - updateToolbarState: function () { - var hasSelection = this.selectedFiles.size > 0; - - this.$btnDownload.prop('disabled', !hasSelection); - this.$btnDelete.prop('disabled', !hasSelection); - - var allSelected = - this.files.length > 0 && - this.selectedFiles.size === this.files.length; - - this.$checkAll.prop('checked', allSelected); - - // 更新未上传文件数量 - var pendingCount = this.files.filter(file => !file.uploaded).length; - this.$pendingCount.text(pendingCount); - - var self = this; - - this.$fileList.find('.file-item').each(function () { - var id = $(this).data('id'); - var isSelected = self.selectedFiles.has(id); - - $(this).find('.file-checkbox') - .prop('checked', isSelected); - - // 添加或移除选中样式 - if (isSelected) { - $(this).addClass('file-item-selected'); - } else { - $(this).removeClass('file-item-selected'); - } - }); - }, - - // ===================== - // 加载文件 - // ===================== - loadFiles: function (files) { - var self = this; - - this.files = files.map(function (file) { - return Object.assign({}, file, { - _id: self.generateUniqueId(), - uploaded: true - }); - }); - - this.selectedFiles.clear(); - - this.renderFileList(); - this.updateToolbarState(); - }, - - // ===================== - // 渲染文件列表 - // ===================== - renderFileList: function () { - var self = this; - - if (this.files.length === 0) { - this.$fileList.hide(); - this.$emptyTip.show(); - return; - } - - this.$fileList.show(); - this.$emptyTip.hide(); - - var html = ''; - $.each(this.files, function (_, file) { - html += self.createFileItem(file); - }); - - this.$fileList.html(html); - this.bindFileItemEvents(); - }, - // ===================== // 格式化文件大小 // ===================== @@ -550,48 +1197,61 @@ // 创建文件项 // ===================== createFileItem: function (file) { - const perm = this.options.permissions || {}; - - const downloadDisabled = !perm.download; - const deleteDisabled = !perm.delete; + 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}`; + content = ` +
+ ${file.name} +
+ ${this.staticIconMap.error || ''} +
+
+ `; } else { content = this.getFileIcon(file.name); } - // 未上传标记 - const pendingBadge = file.uploaded ? '' : ` -
+ const pendingBadge = file.status === 'pending' ? ` +
${this.language.pending}
+ ` : ''; + + const deleteButton = ` +
+ ${this.staticIconMap.delete} +
`; return ` -
- -
${content}
+
+ + +
${content}
${pendingBadge} + ${deleteButton} -
- ${file.name} +
+ ${file.name} ${this.formatFileSize(file.size)} ${this.formatDate(file.uploadDate)}
-
-
+
+
+ ${this.staticIconMap.preview} +
+ +
${this.staticIconMap.download}
-
- ${this.staticIconMap.delete} -
- -
+
${this.staticIconMap.more}
@@ -599,284 +1259,237 @@ `; }, - // ===================== - // 绑定文件项事件 - // ===================== - bindFileItemEvents: function () { - var self = this; + closeActionMenu: function () { + this.actionMenuState.open = false; + this.actionMenuState.fileId = null; + this.actionMenuState.anchorEl = null; + this.actionMenuState.items = []; - // ===================== - // 文件item点击事件 - // ===================== - this.$fileList.off('click', '.file-item') - .on('click', '.file-item', function (e) { - var $checkbox = $(this).find('.file-checkbox'); - $checkbox.prop('checked', !$checkbox.is(':checked')); - $checkbox.trigger('change'); - }); - - this.$fileList.off('click', '.download') - .on('click', '.download', function (e) { - e.preventDefault(); - e.stopPropagation(); - - var id = $(this).closest('.file-item').data('id'); - var file = self.files.find(f => f._id === id); - - if (!file) return; - - if (typeof self.options.onDownload === 'function') { - self.options.onDownload([file]); - return; - } - - self.internalDownload(file); - }); - - this.$fileList.off('change', '.file-checkbox') - .on('change', '.file-checkbox', function () { - var id = $(this).closest('.file-item').data('id'); - - if ($(this).is(':checked')) { - self.selectedFiles.add(id); - } else { - self.selectedFiles.delete(id); - } - - self.updateToolbarState(); - }); - - this.$fileList.off('click', '.delete') - .on('click', '.delete', function (e) { - e.preventDefault(); - e.stopPropagation(); - - var id = $(this).closest('.file-item').data('id'); - var file = self.files.find(f => f._id === id); - - if (!file) return; - - if (typeof self.options.onDelete === 'function') { - self.options.onDelete([file]); - return; - } - - self.internalDelete(file); - }); - - // ===================== - // 更多按钮点击事件(阻止冒泡) - // ===================== - this.$fileList.off('click', '.more') - .on('click', '.more', function (e) { - e.preventDefault(); - e.stopPropagation(); - }); + this.renderActionMenu(); }, + openActionMenu: function ($el, file) { + var items = (this.options.getActionMenu && this.options.getActionMenu(file)) || []; - getUniqueFileName: function (fileName, files) { - // 保存原始文件名 - const originalFileName = fileName; - let uniqueFileName = fileName; - let counter = 1; + this.actionMenuState = { + open: true, + fileId: file.id, + anchorEl: $el[0], + items: items + }; - // 如果文件名已经存在,则在文件名后加上 (1), (2), ... - while (files.has(uniqueFileName)) { - // 基于原始文件名为基础来添加计数器 - const fileNameWithoutExtension = originalFileName.replace(/\.[^/.]+$/, ""); - const extension = originalFileName.slice(originalFileName.lastIndexOf(".")); - uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`; - counter++; - } - - return uniqueFileName; - }, - - getFileBlob: async function (url) { - if (!url) { - throw new Error('无效的文件URL'); - } - - const res = await fetch(url); - - if (!res.ok) { - throw new Error('网络请求失败,状态码:' + res.status); - } - - // ⭐ 比 blob 更轻一点(后续 zip 更快) - const buffer = await res.arrayBuffer(); - - return buffer; + this.renderActionMenu(); }, // ===================== - // 内部下载文件 + // 渲染文件列表 // ===================== - internalDownload: async function (file) { - if (!file) { - console.warn('下载失败:文件对象无效', file); + renderFileList: function () { + if (this.files.length === 0) { + this.$fileList.hide(); + this.$emptyTip.show(); return; } - console.log('开始下载文件:', file.name); + this.$fileList.show(); + this.$emptyTip.hide(); - try { - let blob; - if (file.uploaded && file.url) { - // 已上传的文件,从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; - } + var html = ''; + var self = this; + this.files.forEach(function (file) { + html += self.createFileItem(file); + }); - saveAs(blob, file.name || '未命名文件'); - - } catch (err) { - console.error('文件下载失败:', err); - } + this.$fileList.html(html); + this.bindFileItemEvents(); }, // ===================== - // 内部下载所有文件 + // 更新UI // ===================== - internalDownloadBatch: async function (files, callback) { - if (!files || files.length === 0) return; + updateUI: function () { + var self = this; + var hasSelectedFiles = this.selectedFiles.size > 0; - const zip = new JSZip(); - const addedFiles = new Set(); + // 更新全局按钮状态 + this.$btnSelect?.prop('disabled', !this.hasPermission(null, 'select')); + this.$btnUpload?.prop('disabled', !this.hasPermission(null, 'upload')); - const total = files.length; - let current = 0; - let failedCount = 0; + // 批量下载按钮:需要全局权限且有选中文件 + var canBatchDownload = this.hasPermission(null, 'download') && hasSelectedFiles; + this.$btnDownload?.prop('disabled', !canBatchDownload); - const concurrency = 3; + // 批量删除按钮:需要全局权限且有选中文件 + var canBatchDelete = this.hasPermission(null, 'delete') && hasSelectedFiles; + this.$btnDelete?.prop('disabled', !canBatchDelete); - const downloadOne = async (item) => { - try { - let content; - if (item.uploaded && item.url) { - // 已上传的文件,从URL下载 - content = await this.getFileBlob(item.url); - } else if (item.file) { - // 未上传的文件,使用本地文件对象 - content = await item.file.arrayBuffer(); - } else { - console.warn('跳过失败文件:', item.name, '没有URL或本地文件对象'); - failedCount++; - return; - } + // 更新全选checkbox状态 + var allSelected = this.files.length > 0 && this.selectedFiles.size === this.files.length; + this.$checkAll?.prop('checked', allSelected); - const uniqueFileName = this.getUniqueFileName(item.name, addedFiles); + // 更新待上传数量 + var pendingCount = this.files.filter(file => file.status === 'pending').length; + this.$pendingCount?.text(pendingCount); - zip.file(uniqueFileName, content); + // 更新文件项状态 + 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); - addedFiles.add(uniqueFileName); + if (!file) return; - } catch (err) { - console.warn('跳过失败文件:', item.name, err); - failedCount++; - } finally { - current++; - } - }; + // 更新checkbox选中状态 + var isSelected = self.selectedFiles.has(fileId); + $item.find('.g3-file-checkbox').prop('checked', isSelected); - const runner = async () => { - while (files.length) { - const item = files.shift(); - await downloadOne(item); - } - }; + // 更新按钮权限状态 + var canDownload = self.hasPermission(file, 'download'); + var canView = self.hasPermission(file, 'view'); + var canDelete = self.hasPermission(file, 'delete'); - try { - await Promise.all( - Array.from({ length: concurrency }, runner) - ); + $item.find('.g3-file-action.download') + .toggleClass('is-disabled', !canDownload); - const blob = await zip.generateAsync({ type: "blob" }); + $item.find('.g3-file-action.preview') + .toggleClass('is-disabled', !canView); - 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"); - - console.log( - `下载完成:${total - failedCount}/${total},失败:${failedCount}` - ); - - callback && callback(); - - } catch (err) { - console.error("Batch download failed:", err); - callback && callback(); - } + $item.find('.g3-file-delete-btn') + .toggleClass('is-disabled', !canDelete); + }); }, // ===================== - // 内部删除文件 + // 获取选中文件 // ===================== - internalDelete: function (file) { - if (!file) return; - // 清理本地临时连接 - if (file.url && file.uploaded === false) { + 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(file.url); + URL.revokeObjectURL(url); } catch (e) { - console.warn('Failed to revoke object URL:', e); + console.warn('释放ObjectURL失败:', e); } - } - this.files = this.files.filter(f => f._id !== file._id); - this.selectedFiles.delete(file._id); - this.updateToolbarState(); - this.renderFileList(); - }, + }); + this.objectUrls.clear(); - // ===================== - // 内部删除所有文件 - // ===================== - internalDeleteBatch: function (files) { - if (!files || files.length === 0) return; - files.forEach(file => this.internalDelete(file)); - this.updateToolbarState(); - this.renderFileList(); - }, - // ===================== - // 更新权限 - // ===================== - updatePermissions: function (permissions) { - if (!permissions) return; + // 清理引用 + this.files = []; + this.selectedFiles.clear(); + this.$container.empty(); - this.options.permissions = permissions; - - this.$btnSelect?.prop('disabled', !permissions.select); - this.$btnUpload?.prop('disabled', !permissions.upload); - this.$btnDownload?.prop('disabled', !permissions.download); - this.$btnDelete?.prop('disabled', !permissions.delete); - - this.$fileList.find('.file-action.download') - .toggleClass('is-disabled', !permissions.download); - - this.$fileList.find('.file-action.delete') - .toggleClass('is-disabled', !permissions.delete); - }, + console.log('G3File 组件已销毁'); + } }; // ========================== // jQuery 插件封装 // ========================== $.fn.G3File = function (options) { - var args = Array.prototype.slice.call(arguments, 1); var instance; @@ -884,24 +1497,17 @@ var $this = $(this); instance = $this.data('G3File'); - // ========= 初始化 ========= if (!instance) { - instance = Object.create(G3File); - instance.init(this.id, options); - + instance = new G3File(this, options); $this.data('G3File', instance); - - // ========= 方法调用 ========= } else if (typeof options === 'string') { var method = options; - if (instance[method]) { instance[method].apply(instance, args); } } }); - // ⭐关键:返回 instance(支持 file.xxx) return instance || this; }; diff --git a/code/file-preview/index.html b/code/file-preview/index.html index 87a457d3..cd5416a9 100644 --- a/code/file-preview/index.html +++ b/code/file-preview/index.html @@ -23,16 +23,41 @@ align-items: center; justify-content: center; } + + .button-group { + margin-bottom: 20px; + display: flex; + gap: 10px; + } + + .button-group button { + padding: 8px 16px; + border: none; + border-radius: 4px; + background: #5D87FF; + color: white; + cursor: pointer; + font-size: 14px; + } + + .button-group button:hover { + background: #4a6fcf; + }
- - +
+ + + + + +
-
+
@@ -44,32 +69,133 @@ data: [ { name: 'test.jar', size: 1024000, uploadDate: '2024-01-15T10:30:00', url: 'http://118.89.70.199:9000/test/XI26033384.rar' }, ], - permissions: { + globalPermission: { select: true, upload: true, download: true, delete: true, view: true }, - onUpload: function () { - console.log('Upload clicked'); - alert('上传功能'); + onUpload: function (pendingFiles) { + console.log('上传文件:', pendingFiles); + alert('上传功能,共 ' + pendingFiles.length + ' 个文件待上传'); + }, + onDelete: function (files, callback) { + console.log('删除文件:', files); + if (confirm('确定要删除 ' + files.length + ' 个文件吗?')) { + callback(true); + } else { + callback(false); + } + }, + onPreview: function (files) { + console.log('预览文件:', files); + alert('预览文件: ' + files[0].name); + }, + onSelect: function (files) { + console.log('选择文件:', files); + }, + getActionMenu: function (file) { + const menuItems = []; + + menuItems.push({ + title: '审批', + onClick: function (f) { + console.log('审批文件:', f.name); + alert('审批功能:' + f.name); + } + }); + + menuItems.push({ + title: '修改', + onClick: function (f) { + console.log('修改文件:', f.name); + alert('修改功能:' + f.name); + } + }); + + menuItems.push({ + title: '重命名', + onClick: function (f) { + console.log('重命名文件:', f.name); + const newName = prompt('请输入新文件名:', f.name); + if (newName && newName !== f.name) { + console.log('文件已重命名为:', newName); + } + } + }); + + menuItems.push({ + title: '复制', + disabled: false, + onClick: function (f) { + console.log('复制文件:', f.name); + alert('复制功能:' + f.name); + } + }); + + menuItems.push({ + title: '移动到文件夹', + disabled: false, + onClick: function (f) { + console.log('移动文件:', f.name); + alert('移动功能:' + f.name); + } + }); + + menuItems.push({ + title: '分享', + disabled: false, + onClick: function (f) { + console.log('分享文件:', f.name); + alert('分享功能:' + f.name); + } + }); + + menuItems.push({ + title: '历史版本', + disabled: false, + onClick: function (f) { + console.log('查看历史版本:', f.name); + alert('历史版本功能:' + f.name); + } + }); + + return menuItems; } }); $('#reloadBtn').click(function () { - file.loadFiles([ - { name: '测试文档1.pdf', uploadDate: '2024-01-15T10:30:00', url: '' }, - { name: 'test.png', size: 1024000, uploadDate: '2024-01-15T10:30:00', url: 'http://118.89.70.199:9000/test/DC2A4692147D10C9C7B48A7EA29BCBA8.jpg' }, + file.setFiles([ + { name: '测试文档1.pdf', size: 2048000, uploadDate: '2024-01-15T10:30:00', url: '' }, + { name: 'test.png', size: 1024000, uploadDate: '2024-01-15T10:30:00', url: 'http://118.89.70.:9000/test/DC2A4692147D10C9C7B48A7EA29BCBA8.jpg' }, + { name: '项目报告.docx', size: 524288, uploadDate: '2024-02-20T14:45:00', url: 'http://118.89.70.199:9000/test/report.docx' }, ]); }); $('#reloadBtn2').click(function () { - file.loadFiles([ - { name: '测试文档2.pdf', size: 1024000, uploadDate: '2024-01-15T10:30:00', url: '' }, + file.setFiles([ + { name: '测试文档2.pdf', size: 4096000, uploadDate: '2024-01-15T10:30:00', url: '' }, { name: '项目报告2.docx', size: 524288, uploadDate: '2024-02-20T14:45:00', url: '' }, + { name: '数据表.xlsx', size: 1572864, uploadDate: '2024-03-01T09:00:00', url: 'http://118.89.70.199:9000/test/data.xlsx' }, + { name: '演示文稿.pptx', size: 3145728, uploadDate: '2024-03-05T16:30:00', url: '' }, ]); }); + + $('#addFileBtn').click(function () { + file.$btnSelect.click(); + }); + + $('#clearBtn').click(function () { + file.updateState({ selectedFiles: [] }); + }); + + $('#destroyBtn').click(function () { + if (confirm('确定要销毁组件吗?')) { + file.destroy(); + $('#fileContainer').html('

组件已销毁

'); + } + }); }); diff --git a/code/file-preview/js/FileSaver.min.js b/code/file-preview/js/FileSaver.min.js deleted file mode 100644 index 6d493b29..00000000 --- a/code/file-preview/js/FileSaver.min.js +++ /dev/null @@ -1,3 +0,0 @@ -(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); - -//# sourceMappingURL=FileSaver.min.js.map \ No newline at end of file diff --git a/code/file-preview/test.html b/code/file-preview/test.html new file mode 100644 index 00000000..31e35c46 --- /dev/null +++ b/code/file-preview/test.html @@ -0,0 +1,205 @@ + + + + + + Download Progress UI Comparison + + + + + + +
+ +
File Manager Header
+ +
+ + +
+
+ Downloading + 3 / 10 + report.pdf +
+ +
+ + +
+
+ +
+

File list area...

+

Scroll content...

+

Scroll content...

+

Scroll content...

+
+ +
+ + + +
+ + +
+
+ Downloading + +
+ + +
+
+ +
+
+ 3 / 10 + 30% +
+ +
+ report.pdf +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/code/link56/link56-frontend/src/request/index.ts b/code/link56/link56-frontend/src/request/index.ts index 83ca50f1..caa2ed5a 100644 --- a/code/link56/link56-frontend/src/request/index.ts +++ b/code/link56/link56-frontend/src/request/index.ts @@ -50,7 +50,8 @@ axiosInstance.interceptors.request.use( if (needAuth) { if (!userStore.isLogined) { userStore.clear() - router.push('/login') + // router.push('/login') + router.push('/') return Promise.reject(new Error('尚未登录,请先登录!')) } } @@ -82,7 +83,11 @@ axiosInstance.interceptors.response.use( const resData = response.data as ResponseData if (resData.code === 401) { - router.push('/auth/login') + const userStore = useUserStore() + + userStore.clear() + router.push('/') + // router.push('/auth/login') return Promise.resolve(response.data) } else if (resData.code === 404) {