From 2a00dc7ef47cbc9169fe92072143af6cefaab869 Mon Sep 17 00:00:00 2001 From: oneao Date: Tue, 28 Apr 2026 20:31:31 +0800 Subject: [PATCH] u --- .../documents/optimize-class-names-plan.md | 181 ++++ code/file-preview/g3-file/common.js | 239 ++++++ code/file-preview/g3-file/icon.js | 47 ++ code/file-preview/g3-file/index.css | 329 +++----- code/file-preview/g3-file/index.js | 789 ++++++------------ code/file-preview/g3-file/language.js | 120 +++ code/file-preview/index.html | 10 +- 7 files changed, 980 insertions(+), 735 deletions(-) create mode 100644 code/file-preview/g3-file/.trae/documents/optimize-class-names-plan.md diff --git a/code/file-preview/g3-file/.trae/documents/optimize-class-names-plan.md b/code/file-preview/g3-file/.trae/documents/optimize-class-names-plan.md new file mode 100644 index 00000000..494b8eff --- /dev/null +++ b/code/file-preview/g3-file/.trae/documents/optimize-class-names-plan.md @@ -0,0 +1,181 @@ +# 上传功能实现计划 + +## 1. 需求分析 + +### 1.1 现有功能 + +* 当前 `G3File` 组件的上传功能通过 `onUpload` 回调函数处理 + +* 上传按钮点击时,会获取所有状态为 `pending` 的文件并调用 `onUpload` 回调 + +* 没有内置的上传实现,完全依赖外部回调 + +### 1.2 需求目标 + +* 实现阿里云内部上传方法 + +* 保留外部方法,允许用户动态获取阿里云key等信息 + +* 支持外部自定义上传方法 + +* 只上传未上传(状态为 `pending`)的文件 + +* 上传成功后,将上传成功的 fileitem 传给外部调用,上传成功一个就触发一次 + +## 2. 技术方案 + +### 2.1 核心功能设计 + +1. **上传配置**:添加上传相关的配置选项,包括阿里云上传配置和自定义上传方法 +2. **上传方法**:实现阿里云内部上传方法和外部自定义上传方法的支持 +3. **上传流程**:优化上传流程,只上传未上传的文件,上传成功后触发回调 +4. **状态管理**:管理文件的上传状态,包括 `pending`、`uploading`、`uploaded`、`failed` 等 + +### 2.2 配置选项 + +* `uploadConfig`:上传配置对象,包含阿里云上传配置和自定义上传方法 + +* `onUploadSuccess`:上传成功回调,上传成功一个文件就触发一次 + +* `onUploadError`:上传失败回调 + +* `onUploadComplete`:所有文件上传完成回调 + +### 2.3 上传流程 + +1. 用户点击上传按钮 +2. 组件获取所有状态为 `pending` 的文件 +3. 检查是否配置了自定义上传方法,如果有则使用自定义方法 +4. 如果没有自定义上传方法,则使用阿里云内部上传方法 +5. 逐个上传文件,更新文件状态为 `uploading` +6. 上传成功后,更新文件状态为 `uploaded`,并触发 `onUploadSuccess` 回调 +7. 上传失败后,更新文件状态为 `failed`,并触发 `onUploadError` 回调 +8. 所有文件上传完成后,触发 `onUploadComplete` 回调 + +## 3. 实现步骤 + +### 3.1 修改 HTML 结构 + +* 无需修改 HTML 结构,使用现有的上传按钮 + +### 3.2 扩展 JavaScript 功能 + +1. **添加上传配置选项**:在 `defaults` 中添加上传相关的配置选项 +2. **实现上传方法**: + + * `internalUpload`:内部上传方法,支持阿里云上传 + + * `handleUpload`:处理上传逻辑,支持自定义上传方法 +3. **更新上传流程**:修改 `$btnUpload` 的点击事件处理,使用新的上传逻辑 +4. **添加回调方法**:实现 `onUploadSuccess`、`onUploadError`、`onUploadComplete` 回调 + +### 3.3 优化 CSS 样式 + +* 为上传状态添加相应的样式,如上传中、上传失败等 + +### 3.4 测试和验证 + +1. 测试阿里云内部上传功能 +2. 测试外部自定义上传方法 +3. 测试上传状态管理 +4. 测试回调函数触发 + +## 4. 预期效果 + +### 4.1 上传功能 + +* 支持阿里云内部上传 + +* 支持外部自定义上传方法 + +* 只上传未上传的文件 + +* 上传成功后触发回调 + +### 4.2 状态管理 + +* 清晰的上传状态管理 + +* 上传中、上传成功、上传失败等状态的视觉反馈 + +### 4.3 回调机制 + +* 上传成功一个文件就触发一次 `onUploadSuccess` 回调 + +* 上传失败触发 `onUploadError` 回调 + +* 所有文件上传完成触发 `onUploadComplete` 回调 + +## 5. 风险评估 + +### 5.1 潜在风险 + +* 阿里云上传配置复杂,可能出现配置错误 + +* 自定义上传方法可能与内部上传逻辑冲突 + +* 上传过程中可能出现网络错误 + +### 5.2 风险缓解措施 + +* 提供详细的配置文档 + +* 确保自定义上传方法与内部上传逻辑的兼容性 + +* 添加错误处理和重试机制 + +## 6. 实现细节 + +### 6.1 上传配置 + +```javascript +uploadConfig: { + aliyun: { + // 阿里云配置是可选的 + // getUploadToken 是必须的,用于动态获取上传凭证 + getUploadToken: null // 动态获取上传凭证的函数,返回 Promise + }, + customUpload: null // 自定义上传方法 +} +``` + +### 6.2 回调函数 + +```javascript +onUploadSuccess: function(file) { + // 上传成功回调 +}, +onUploadError: function(file, error) { + // 上传失败回调 +}, +onUploadComplete: function(successCount, failCount) { + // 所有文件上传完成回调 +} +``` + +### 6.3 上传方法 + +* `internalUpload(file, callback)`:内部上传方法 + +* `handleUpload(files)`:处理上传逻辑 + +## 7. 代码结构 + +### 7.1 主要修改文件 + +* `custom-file/index.js`:扩展上传功能 + +### 7.2 新增方法 + +* `internalUpload`:内部上传方法 + +* `handleUpload`:处理上传逻辑 + +* `updateFileStatus`:更新文件状态 + +### 7.3 修改方法 + +* `init`:初始化上传配置 + +* `bindEvents`:更新上传按钮的点击事件处理 + diff --git a/code/file-preview/g3-file/common.js b/code/file-preview/g3-file/common.js index e69de29b..b03f5070 100644 --- a/code/file-preview/g3-file/common.js +++ b/code/file-preview/g3-file/common.js @@ -0,0 +1,239 @@ +var G3FileUtils = { + /** + * 格式化文件大小 + * @param {number} bytes - 文件大小(字节) + * @returns {string} 格式化后的大小 + */ + formatFileSize: function (bytes) { + var numBytes = typeof bytes === "string" ? parseFloat(bytes) : bytes; + if ( + numBytes === null || + numBytes === undefined || + isNaN(numBytes) || + numBytes <= 0 + ) { + return "-"; + } + + var k = 1024; + var sizes = ["B", "KB", "MB", "GB", "TB"]; + + var i = Math.floor(Math.log(numBytes) / Math.log(k)); + i = Math.max(0, Math.min(i, sizes.length - 1)); + + return (numBytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i]; + }, + + /** + * 格式化日期 + * @param {string} dateStr - 日期字符串 + * @returns {string} 格式化后的日期 + */ + formatDate: function (dateStr) { + if (!dateStr) return "-"; + + var date = new Date(dateStr); + + if (isNaN(date.getTime())) { + return "-"; + } + + var year = date.getFullYear(); + var month = ("0" + (date.getMonth() + 1)).slice(-2); + var day = ("0" + date.getDate()).slice(-2); + var hours = ("0" + date.getHours()).slice(-2); + var minutes = ("0" + date.getMinutes()).slice(-2); + var seconds = ("0" + date.getSeconds()).slice(-2); + + return ( + year + + "-" + + month + + "-" + + day + + " " + + hours + + ":" + + minutes + + ":" + + seconds + ); + }, + + /** + * 判断是否为图片文件 + * @param {string} filename - 文件名 + * @returns {boolean} 是否为图片文件 + */ + isImageFile: function (filename) { + if (!filename || typeof filename !== "string") return false; + + const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase(); + const imageExts = new Set(["jpg", "jpeg", "png", "gif", "bmp", "webp"]); + + return imageExts.has(ext); + }, + + /** + * 获取文件图标 + * @param {string} filename - 文件名 + * @returns {string} 图标HTML + */ + getFileIcon: function (filename) { + var ext = filename.split(".").pop().toLowerCase(); + + if (this.isImageFile(filename)) { + return ""; + } + + // 根据不同的文件类型显示不同的图标 + const audioTypes = ["mp3", "wav", "aac", "flac", "ogg"]; + if (audioTypes.includes(ext)) { + return G3FileIcons.file.audio; + } + + // 视频类型 + const videoTypes = ["mp4", "avi", "mkv", "mov", "wmv"]; + if (videoTypes.includes(ext)) { + return G3FileIcons.file.video; + } + + if (ext === "txt") { + return G3FileIcons.file.txt; + } + + if (ext === "pdf") { + return G3FileIcons.file.pdf; + } + + // 文档类型 + const documentTypes = ["docx", "xlsx", "pptx", "office"]; + if (documentTypes.includes(ext)) { + return G3FileIcons.file.docs; + } + + // 压缩包类型 + const archiveTypes = ["zip", "rar", "tar", "gz"]; + if (archiveTypes.includes(ext)) { + return G3FileIcons.file.zip; + } + + return G3FileIcons.file.default; + }, + + /** + * 获取唯一文件名 + * @param {string} fileName - 原始文件名 + * @param {Set} files - 已存在的文件名集合 + * @returns {string} 唯一文件名 + */ + 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; + }, + + /** + * 压缩图片 + * @param {File} file - 图片文件 + * @param {Object} options - 压缩选项 + * @returns {Promise} 压缩后的文件 + */ + 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; + + 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); + }); + + let quality = 0.92; + + if (originSize > 4 * 1024 * 1024) { + quality = 0.88; + } + + const blob = await getBlob(quality); + + if (blob && blob.size < minSize) { + const safe = await getBlob(0.97); + return safe || file; + } + + return blob || file; + }, + + /** + * 生成唯一ID + * @returns {string} 唯一ID + */ + generateUniqueId: function () { + return "file_" + Math.random().toString(36).slice(2) + Date.now(); + }, + + /** + * Base64 编码 + * @param {string} str - 字符串 + * @returns {string} Base64 编码结果 + */ + base64Encode: function (str) { + return btoa( + encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { + return String.fromCharCode("0x" + p1); + }), + ); + }, +}; diff --git a/code/file-preview/g3-file/icon.js b/code/file-preview/g3-file/icon.js index e69de29b..08a48aa3 100644 --- a/code/file-preview/g3-file/icon.js +++ b/code/file-preview/g3-file/icon.js @@ -0,0 +1,47 @@ +var G3FileIcons = { + action: { + download: ` + + `, + delete: ` + + + + `, + select: ` + + `, + upload: ` + + `, + preview: ` + + `, + more: ` + + `, + + }, + file: { + default: '', + audio:``, + video:``, + txt:``, + pdf:``, + docs:``, + zip:``, + }, + preview: { + prev: '', + next: '', + imgLoadError:` + + ` + }, + toast: { + success: '', + error: '', + info: '', + warning: '', + }, +}; \ No newline at end of file diff --git a/code/file-preview/g3-file/index.css b/code/file-preview/g3-file/index.css index 68f5b8e7..74ea21ab 100644 --- a/code/file-preview/g3-file/index.css +++ b/code/file-preview/g3-file/index.css @@ -12,7 +12,7 @@ border: 2px dashed #165dff; } -.g3-file-left-panel { +.g3-left-panel { width: 50%; display: flex; flex-direction: column; @@ -20,7 +20,7 @@ min-width: 300px; } -.g3-file-container .header { +.g3-header { display: flex; align-items: center; height: 50px; @@ -29,7 +29,7 @@ border-bottom: 1px solid #eef0f2; } -.g3-file-container .main { +.g3-main { flex: 1; min-height: 0; padding: 4px 0px; @@ -39,11 +39,11 @@ align-content: flex-start; } -.g3-file-container .main > * { +.g3-main > * { margin: 10px; } -.g3-file-container .empty-tip { +.g3-empty-tip { flex: 1; min-height: 0; display: flex; @@ -54,7 +54,7 @@ color: #999; } -.g3-file-container .footer { +.g3-footer { display: flex; align-items: center; justify-content: space-between; @@ -63,26 +63,26 @@ border-top: 1px solid #eee; } -.g3-file-container .header, -.g3-file-container .footer { +.g3-header, +.g3-footer { flex-shrink: 0; } -.footer div { +.g3-footer div { display: flex; align-items: center; } -.footer .pending-count { +.g3-pending-count { margin-right: 12px; font-size: 14px; } -.footer .pending-count .pending-number { +.g3-pending-count .g3-pending-number { margin-left: 4px; } -.g3-file-container .check-all { +.g3-check-all { display: flex; align-items: center; margin-right: 12px; @@ -90,17 +90,17 @@ cursor: pointer; } -.g3-file-container .check-all input { +.g3-check-all input { width: 16px; height: 16px; } -.g3-file-container .check-all span { +.g3-check-all span { margin-left: 4px; color: #333; } -.g3-file-container button { +.g3-btn { font-size: 13px; border-radius: 4px; background: #165dff; @@ -117,32 +117,32 @@ justify-content: center; } -.g3-file-container .g3-file-left-panel button svg { +.g3-btn svg { margin-right: 4px; } -.g3-file-container button + button { +.g3-btn + .g3-btn { margin-left: 12px; } -.g3-file-container button:hover:not(:disabled) { +.g3-btn:hover:not(:disabled) { filter: brightness(1.2); } -.g3-file-container button:active:not(:disabled) { +.g3-btn:active:not(:disabled) { filter: brightness(0.9); } -.g3-file-container button:disabled { +.g3-btn:disabled { opacity: 0.5; cursor: not-allowed; } -.g3-file-container .delete-all { +.g3-btn-delete-all { background: #ff4d4f; } -.g3-file-container .g3-file-item { +.g3-file-item { width: calc(50% - 26px); position: relative; cursor: pointer; @@ -152,11 +152,11 @@ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); } -.g3-file-container .g3-file-item:hover { +.g3-file-item:hover { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); } -.g3-file-item .g3-file-preview { +.g3-file-preview { height: 100px; width: 100%; display: flex; @@ -165,17 +165,17 @@ background-color: #fafbfc; } -.g3-file-item .g3-file-preview .g3-file-img { +.g3-file-preview .g3-file-img { width: 100%; height: 100%; } -.g3-file-item .g3-file-preview svg { +.g3-file-preview svg { height: 86px; width: 100%; } -.g3-file-item .g3-file-preview .g3-img-container { +.g3-file-preview .g3-img-container { width: 100%; height: 100%; position: relative; @@ -184,7 +184,7 @@ justify-content: center; } -.g3-file-item .g3-file-preview .g3-img-container.is-error .g3-img-error { +.g3-file-preview .g3-img-container.is-error .g3-img-error { display: flex; align-items: center; justify-content: center; @@ -193,7 +193,7 @@ background-color: #f0f2f5; } -.g3-file-item .g3-file-preview .g3-img-container .g3-img-error { +.g3-file-preview .g3-img-container .g3-img-error { display: none; position: absolute; top: 0; @@ -202,13 +202,13 @@ height: 100%; } -.g3-file-item .g3-file-preview .g3-img-container .g3-img-error svg { +.g3-file-preview .g3-img-container .g3-img-error svg { width: 48px; height: 48px; color: #999; } -.g3-file-item .g3-file-info { +.g3-file-info { padding: 8px; font-size: 12px; color: #999; @@ -221,7 +221,7 @@ box-sizing: border-box; } -.g3-file-item .g3-file-info span { +.g3-file-info span { display: block; width: 100%; text-align: center; @@ -232,12 +232,12 @@ color: #888; } -.g3-file-item .g3-file-info .g3-file-name { +.g3-file-info .g3-file-name { font-size: 13px; color: #000000; } -.g3-file-item .g3-file-actions { +.g3-file-actions { display: flex; align-items: center; justify-content: flex-end; @@ -245,7 +245,7 @@ border-top: 1px solid #f0f2f5; } -.g3-file-item .g3-file-action { +.g3-file-action { padding: 8px 2px; flex: 1; display: flex; @@ -257,37 +257,37 @@ border-right: 1px solid #f0f2f5; } -.g3-file-item .g3-file-action:hover { +.g3-file-action:hover { background-color: #f5f6f8; color: #333; } -.g3-file-item .g3-file-action:active { +.g3-file-action:active { background: rgba(22, 93, 255, 0.12); } -.g3-file-item .g3-file-action.is-disabled { +.g3-file-action.is-disabled { opacity: 0.4; pointer-events: none; cursor: not-allowed; } -.g3-file-item .g3-file-action:first-child { +.g3-file-action:first-child { border-radius: 0 0 0 8px; } -.g3-file-item .g3-file-action:last-child { +.g3-file-action:last-child { border-right: none; border-radius: 0 0 8px 0; } -.g3-file-item .g3-file-action svg { +.g3-file-action svg { width: 20px; height: 20px; color: #666; } -.g3-file-item .g3-file-checkbox { +.g3-file-checkbox { position: absolute; top: 8px; left: 8px; @@ -298,7 +298,7 @@ z-index: 10; } -.g3-file-item .g3-file-checkbox + .g3-checkbox-label { +.g3-checkbox-label { position: absolute; top: 8px; left: 8px; @@ -312,12 +312,12 @@ z-index: 5; } -.g3-file-item .g3-file-checkbox:checked + .g3-checkbox-label { +.g3-file-checkbox:checked + .g3-checkbox-label { background: #165dff; border-color: #165dff; } -.g3-file-item .g3-file-checkbox:checked + .g3-checkbox-label::after { +.g3-file-checkbox:checked + .g3-checkbox-label::after { content: ""; position: absolute; top: 2px; @@ -329,65 +329,44 @@ transform: rotate(45deg); } -.g3-file-item .g3-file-checkbox:hover + .g3-checkbox-label { +.g3-file-checkbox:hover + .g3-checkbox-label { border-color: #165dff; box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1); } -.g3-file-item .g3-file-checkbox:disabled + .g3-checkbox-label { +.g3-file-checkbox:disabled + .g3-checkbox-label { opacity: 0.4; cursor: not-allowed; } -.g3-file-item .g3-file-pending-badge { +.g3-status-badge { position: absolute; top: 6px; right: 6px; + color: white; + font-size: 10px; + padding: 2px 6px; + border-radius: 10px; + z-index: 1; +} + +.g3-status-pending { background: #ff9800; - color: white; - font-size: 10px; - padding: 2px 6px; - border-radius: 10px; - z-index: 1; } -.g3-file-item .g3-file-uploading-badge { - position: absolute; - top: 6px; - right: 6px; +.g3-status-uploading { background: #165dff; - color: white; - font-size: 10px; - padding: 2px 6px; - border-radius: 10px; - z-index: 1; } -.g3-file-item .g3-file-failed-badge { - position: absolute; - top: 6px; - right: 6px; +.g3-status-failed { background: #ff4d4f; - color: white; - font-size: 10px; - padding: 2px 6px; - border-radius: 10px; - z-index: 1; } -.g3-file-item .g3-file-uploaded-badge { - position: absolute; - top: 6px; - right: 6px; +.g3-status-uploaded { background: #52c41a; - color: white; - font-size: 10px; - padding: 2px 6px; - border-radius: 10px; - z-index: 1; } -.g3-file-container .g3-file-delete-btn { +.g3-file-delete-btn { position: absolute; top: -8px; right: -8px; @@ -406,18 +385,18 @@ transition: all 0.2s ease; } -.g3-file-container .g3-file-delete-btn:hover { +.g3-file-delete-btn:hover { background: #ff4d4f; color: white; } -.g3-file-container .g3-file-item .g3-file-delete-btn.is-disabled { +.g3-file-item .g3-file-delete-btn.is-disabled { opacity: 0.4; pointer-events: none; cursor: not-allowed; } -.g3-file-preview-panel { +.g3-preview-panel { flex: 1; display: flex; flex-direction: column; @@ -436,13 +415,21 @@ flex-shrink: 0; } -.g3-preview-toolbar .preview-nav { +.g3-preview-nav { display: flex; align-items: center; - gap: 8px; + gap: 12px; } -.g3-preview-toolbar .preview-btn { +.g3-preview-nav .g3-preview-counter { + font-size: 14px; + color: #666; + font-weight: 500; + min-width: 50px; + text-align: center; +} + +.g3-preview-btn { width: 32px; height: 32px; border: 1px solid #d9d9d9; @@ -456,45 +443,34 @@ padding: 0; } -.g3-preview-toolbar .preview-btn:hover:not(:disabled) { +.g3-preview-btn:hover:not(:disabled) { border-color: #165dff; background: rgba(22, 93, 255, 0.05); } -.g3-preview-toolbar .preview-btn:disabled { +.g3-preview-btn:disabled { opacity: 0.4; cursor: not-allowed; } -.g3-preview-toolbar .preview-btn svg { +.g3-preview-btn svg { width: 16px; height: 16px; color: #666; } -.g3-preview-toolbar .preview-btn:hover:not(:disabled) svg { +.g3-preview-btn:hover:not(:disabled) svg { color: #165dff; } -.g3-preview-toolbar .preview-info { - display: flex; - flex-direction: column; - align-items: flex-end; -} - -.g3-preview-toolbar .preview-info .preview-name { - font-size: 13px; +.g3-preview-name { + font-size: 15px; color: #333; - font-weight: 500; + font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 200px; -} - -.g3-preview-toolbar .preview-info .preview-counter { - font-size: 12px; - color: #999; + max-width: 250px; } .g3-preview-content { @@ -508,27 +484,10 @@ overflow: auto; } -.g3-preview-content .preview-placeholder { +.g3-preview-content .g3-preview-placeholder { text-align: center; color: #999; -} - -.g3-preview-content .preview-placeholder svg { - width: 80px; - height: 80px; - margin-bottom: 16px; - opacity: 0.5; -} - -.g3-preview-content .preview-placeholder h3 { - font-size: 14px; - margin-bottom: 8px; - color: #666; -} - -.g3-preview-content .preview-placeholder p { - font-size: 12px; - line-height: 1.5; + font-size: 24px; } .g3-preview-content .preview-image { @@ -543,7 +502,7 @@ color: #ccc; } -.g3-preview-content .preview-iframe { +.g3-preview-content .g3-preview-iframe { width: 100%; height: 100%; border: none; @@ -566,52 +525,24 @@ color: #666; } -.g3-preview-content .preview-file-info .status-uploaded { +.g3-preview-content .preview-file-info .g3-status-uploaded { color: #52c41a; font-weight: 500; } -.g3-preview-content .preview-file-info .status-pending { +.g3-preview-content .preview-file-info .g3-status-pending { color: #ff9800; font-weight: 500; } -.g3-preview-content .preview-file-info .cannot-preview { - margin-top: 12px; - padding: 8px 12px; - background: rgba(255, 77, 79, 0.08); - color: #ff4d4f; - border-radius: 4px; - font-size: 12px; -} - -.g3-preview-content .preview-file-info .upload-tip { - margin-top: 12px; - padding: 8px 12px; - background: rgba(255, 152, 0, 0.08); - color: #ff9800; - border-radius: 4px; - font-size: 12px; -} - -.g3-preview-content .url-error { - margin-top: 12px; - padding: 8px 12px; - background: rgba(255, 77, 79, 0.08); - color: #ff4d4f; - border-radius: 4px; - font-size: 12px; -} - .g3-global-menu { - position: absolute; + position: fixed; 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; } @@ -637,6 +568,12 @@ background-color: #e8e8e8; } +.g3-global-menu .g3-action-menu-item.is-disabled { + opacity: 0.4; + pointer-events: none; + cursor: not-allowed; +} + .g3-toast-container { position: fixed; top: 20px; @@ -686,7 +623,7 @@ animation: toastSlideOut 0.3s ease-out forwards; } -.g3-toast .toast-icon { +.g3-toast-icon { flex-shrink: 0; width: 20px; height: 20px; @@ -697,69 +634,77 @@ border-radius: 50%; } -.g3-toast.toast-success { +.g3-toast-success { border-left-color: #52c41a; } -.g3-toast.toast-success .toast-icon { + +.g3-toast-success .g3-toast-icon { background: rgba(82, 196, 26, 0.1); color: #52c41a; } -.g3-toast.toast-success .toast-title { + +.g3-toast-success .g3-toast-title { color: #52c41a; } -.g3-toast.toast-error { +.g3-toast-error { border-left-color: #ff4d4f; } -.g3-toast.toast-error .toast-icon { + +.g3-toast-error .g3-toast-icon { background: rgba(255, 77, 79, 0.1); color: #ff4d4f; } -.g3-toast.toast-error .toast-title { + +.g3-toast-error .g3-toast-title { color: #ff4d4f; } -.g3-toast.toast-info { +.g3-toast-info { border-left-color: #165dff; } -.g3-toast.toast-info .toast-icon { + +.g3-toast-info .g3-toast-icon { background: rgba(22, 93, 255, 0.1); color: #165dff; } -.g3-toast.toast-info .toast-title { + +.g3-toast-info .g3-toast-title { color: #165dff; } -.g3-toast.toast-warning { +.g3-toast-warning { border-left-color: #ff9800; } -.g3-toast.toast-warning .toast-icon { + +.g3-toast-warning .g3-toast-icon { background: rgba(255, 152, 0, 0.1); color: #ff9800; } -.g3-toast.toast-warning .toast-title { + +.g3-toast-warning .g3-toast-title { color: #ff9800; } -.g3-toast .toast-content { +.g3-toast-content { flex: 1; min-width: 0; } -.g3-toast .toast-title { +.g3-toast-title { font-size: 14px; font-weight: 500; color: #333; margin-bottom: 4px; } -.g3-toast .toast-message { +.g3-toast-message { font-size: 12px; color: #666; line-height: 1.4; } -.g3-toast .toast-close { +.g3-toast-close { flex-shrink: 0; margin-left: 12px; width: 20px; @@ -775,7 +720,7 @@ transition: all 0.2s ease; } -.g3-toast .toast-close:hover { +.g3-toast-close:hover { background: #f0f2f5; color: #333; } @@ -809,7 +754,7 @@ display: block; } -.g3-task-center .task-header { +.g3-task-header { display: flex; align-items: center; justify-content: space-between; @@ -819,13 +764,13 @@ border-radius: 8px 8px 0 0; } -.g3-task-center .task-header .task-title { +.g3-task-header .g3-task-title { font-size: 14px; font-weight: 500; color: #333; } -.g3-task-center .task-header .task-close { +.g3-task-header .g3-task-close { width: 24px; height: 24px; border: none; @@ -840,16 +785,16 @@ transition: all 0.2s ease; } -.g3-task-center .task-header .task-close:hover { +.g3-task-header .g3-task-close:hover { background: #f0f2f5; color: #333; } -.g3-task-center .task-body { +.g3-task-body { padding: 16px; } -.g3-task-center .current-item { +.g3-current-item { font-size: 13px; color: #666; margin-bottom: 12px; @@ -858,7 +803,7 @@ text-overflow: ellipsis; } -.g3-task-center .progress-bar { +.g3-progress-bar { width: 100%; height: 6px; background: #f0f2f5; @@ -867,52 +812,52 @@ margin-bottom: 12px; } -.g3-task-center .progress-bar .progress-inner { +.g3-progress-bar .g3-progress-inner { height: 100%; background: #165dff; border-radius: 3px; transition: width 0.3s ease; } -.g3-task-center .progress-text { +.g3-progress-text { font-size: 12px; color: #999; text-align: right; margin-bottom: 16px; } -.g3-task-center .result-section { +.g3-result-section { display: flex; justify-content: space-around; padding-top: 12px; border-top: 1px solid #f0f2f5; } -.g3-task-center .result-item { +.g3-result-item { text-align: center; } -.g3-task-center .result-item .result-value { +.g3-result-item .g3-result-value { font-size: 18px; font-weight: 500; color: #333; } -.g3-task-center .result-item .result-label { +.g3-result-item .g3-result-label { font-size: 12px; color: #999; margin-top: 4px; } -.g3-task-center .result-item.result-success .result-value { +.g3-result-item.g3-result-success .g3-result-value { color: #52c41a; } -.g3-task-center .result-item.result-failed .result-value { +.g3-result-item.g3-result-failed .g3-result-value { color: #ff4d4f; } -.g3-task-center .complete-button { +.g3-complete-button { width: 100%; margin-top: 12px; padding: 8px; @@ -925,7 +870,7 @@ transition: all 0.2s ease; } -.g3-task-center .complete-button:hover { +.g3-complete-button:hover { filter: brightness(1.1); } diff --git a/code/file-preview/g3-file/index.js b/code/file-preview/g3-file/index.js index 350f8146..cbf47bed 100644 --- a/code/file-preview/g3-file/index.js +++ b/code/file-preview/g3-file/index.js @@ -8,12 +8,12 @@ // 实例隔离 this.$container = $(container); this.options = $.extend({}, this.defaults, options); - this.instanceId = this.generateUniqueId(); + this.instanceId = G3FileUtils.generateUniqueId(); this.files = []; this.selectedFiles = new Set(); this.objectUrls = new Map(); this.language = - this.languages[this.options.language] || this.languages["zh-CN"]; + G3FileLanguages[this.options.language] || G3FileLanguages["zh-CN"]; // 操作菜单状态 this.actionMenuState = { @@ -96,155 +96,6 @@ 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: "预览", - filePreview: "文件预览", - fileName: "文件名", - fileSize: "文件大小", - uploadDate: "上传时间", - fileStatus: "状态", - notUploaded: "未上传", - uploaded: "已上传", - cannotPreview: "该类型文件不支持预览", - previewCount: "{current}/{total}", - uploadToPreview: "请上传后预览", - downloadingStatus: "下载中:", - downloadingFile: "正在下载:", - downloadComplete: "下载完成", - downloadResult: "共{total}文件,{success}成功,{failed}失败", - close: "关闭", - batchDownload: "批量下载", - batchUpload: "批量上传", - error: "错误", - downloadError: "下载失败,请检查文件是否有效", - toast: { - success: "成功", - error: "错误", - info: "提示", - warning: "警告", - }, - task: { - total: "总计", - success: "成功", - failed: "失败", - currentItem: "当前项", - 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", - filePreview: "File Preview", - fileName: "File Name", - fileSize: "File Size", - uploadDate: "Upload Date", - fileStatus: "Status", - notUploaded: "Not uploaded", - uploaded: "Uploaded", - cannotPreview: "This file type is not previewable", - previewCount: "{current}/{total}", - uploadToPreview: "Please upload to preview", - downloadingStatus: "Downloading:", - downloadingFile: "Downloading:", - downloadComplete: "Download complete", - downloadResult: - "Total {total} files, {success} succeeded, {failed} failed", - close: "Close", - batchDownload: "Batch Download", - batchUpload: "Batch Upload", - error: "Error", - downloadError: "Download failed, please check if the file is valid", - toast: { - success: "Success", - error: "Error", - info: "Info", - warning: "Warning", - }, - task: { - total: "Total", - success: "Success", - failed: "Failed", - currentItem: "Current", - close: "Close", - }, - }, - }, - - /** - * 静态图标映射 - */ - staticIconMap: { - download: ` - - `, - delete: ` - - - - `, - select: ` - - `, - upload: ` - - `, - preview: ` - `, - more: ` - - `, - }, - // ============================================================================ // 初始化和核心方法 // ============================================================================ @@ -256,7 +107,6 @@ init: function () { var self = this; - self.render(); self.bindEvents(); self.updateUI(); @@ -287,7 +137,9 @@ if (keyData && keyData.accessid && keyData.host) { this.aliyunKeyCache = { data: keyData, - expireTime: keyData.expire ? keyData.expire * 1000 : Date.now() + 3600000, + expireTime: keyData.expire + ? keyData.expire * 1000 + : Date.now() + 3600000, }; console.log("[G3File] 阿里云 OSS 凭证预加载成功"); } @@ -323,7 +175,9 @@ this.aliyunKeyCache = { data: keyData, - expireTime: keyData.expire ? keyData.expire * 1000 : Date.now() + 3600000, + expireTime: keyData.expire + ? keyData.expire * 1000 + : Date.now() + 3600000, }; console.log("[G3File] 阿里云 OSS 凭证获取成功"); @@ -343,7 +197,7 @@ generateFileKey: function (keyData, filename) { const dir = keyData.dir || ""; const ext = filename.slice(filename.lastIndexOf(".")) || ""; - const uuid = this.generateUniqueId().replace("file_", ""); + const uuid = G3FileUtils.generateUniqueId().replace("file_", ""); return dir + uuid + ext; }, @@ -365,185 +219,7 @@ * @returns {string} 图标HTML */ getFileIcon: function (filename) { - var ext = filename.split(".").pop().toLowerCase(); - var iconMap = { - pdf: '', - png: '', - jpg: '', - default: - '', - }; - - if (this.isImageFile(filename)) { - return ""; - } - - return iconMap[ext] || iconMap.default; - }, - - /** - * 压缩图片 - * @param {File} file - 图片文件 - * @param {Object} options - 压缩选项 - * @returns {Promise} 压缩后的文件 - */ - 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; - - 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); - }); - - let quality = 0.92; - - if (originSize > 4 * 1024 * 1024) { - quality = 0.88; - } - - const blob = await getBlob(quality); - - if (blob && blob.size < minSize) { - const safe = await getBlob(0.97); - return safe || file; - } - - return blob || file; - }, - - /** - * 判断是否为图片文件 - * @param {string} filename - 文件名 - * @returns {boolean} 是否为图片文件 - */ - isImageFile: function (filename) { - if (!filename || typeof filename !== "string") return false; - - const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase(); - const imageExts = new Set(["jpg", "jpeg", "png", "gif", "bmp", "webp"]); - - return imageExts.has(ext); - }, - - /** - * 格式化文件大小 - * @param {number} bytes - 文件大小(字节) - * @returns {string} 格式化后的大小 - */ - formatFileSize: function (bytes) { - var numBytes = typeof bytes === 'string' ? parseFloat(bytes) : bytes; - if (numBytes === null || numBytes === undefined || isNaN(numBytes) || numBytes <= 0) { - return "-"; - } - - var k = 1024; - var sizes = ["B", "KB", "MB", "GB", "TB"]; - - var i = Math.floor(Math.log(numBytes) / Math.log(k)); - i = Math.max(0, Math.min(i, sizes.length - 1)); - - return (numBytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i]; - }, - - /** - * 格式化日期 - * @param {string} dateStr - 日期字符串 - * @returns {string} 格式化后的日期 - */ - formatDate: function (dateStr) { - if (!dateStr) return "-"; - - var date = new Date(dateStr); - - if (isNaN(date.getTime())) { - return "-"; - } - - var year = date.getFullYear(); - var month = ("0" + (date.getMonth() + 1)).slice(-2); - var day = ("0" + date.getDate()).slice(-2); - var hours = ("0" + date.getHours()).slice(-2); - var minutes = ("0" + date.getMinutes()).slice(-2); - var seconds = ("0" + date.getSeconds()).slice(-2); - - return ( - year + - "-" + - month + - "-" + - day + - " " + - hours + - ":" + - minutes + - ":" + - seconds - ); - }, - - /** - * 获取唯一文件名 - * @param {string} fileName - 原始文件名 - * @param {Set} files - 已存在的文件名集合 - * @returns {string} 唯一文件名 - */ - 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; + return G3FileUtils.getFileIcon(filename); }, /** @@ -586,66 +262,62 @@ render: function () { const html = `
-
-
-
- +
+
+
+ ${this.language.selectAll}
- -
- -
+ +
${this.language.emptyTip1} ${this.language.emptyTip2} ${this.language.emptyTip3}
-