feat(上传): 替换浏览器MD5为七牛云ETag计算方案
添加 rusha 依赖并实现 qiniuFileHash 工具函数 统一所有文件上传场景使用七牛云ETag计算方式 优化大文件分块计算性能并添加进度回调
Showing
9 changed files
with
228 additions
and
68 deletions
| ... | @@ -55,6 +55,7 @@ | ... | @@ -55,6 +55,7 @@ |
| 55 | "less": "^4.2.2", | 55 | "less": "^4.2.2", |
| 56 | "postcss": "^8.4.35", | 56 | "postcss": "^8.4.35", |
| 57 | "qs": "^6.14.0", | 57 | "qs": "^6.14.0", |
| 58 | + "rusha": "^0.8.14", | ||
| 58 | "tailwindcss": "^3.4.1", | 59 | "tailwindcss": "^3.4.1", |
| 59 | "unplugin-auto-import": "^19.1.1", | 60 | "unplugin-auto-import": "^19.1.1", |
| 60 | "unplugin-vue-components": "^28.4.1", | 61 | "unplugin-vue-components": "^28.4.1", | ... | ... |
| ... | @@ -32,7 +32,6 @@ declare module 'vue' { | ... | @@ -32,7 +32,6 @@ declare module 'vue' { |
| 32 | RouterView: typeof import('vue-router')['RouterView'] | 32 | RouterView: typeof import('vue-router')['RouterView'] |
| 33 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] | 33 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] |
| 34 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] | 34 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] |
| 35 | - TeacherFilter: typeof import('./components/ui/TeacherFilter.vue')['default'] | ||
| 36 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] | 35 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] |
| 37 | UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] | 36 | UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] |
| 38 | UserAgreement: typeof import('./components/ui/UserAgreement.vue')['default'] | 37 | UserAgreement: typeof import('./components/ui/UserAgreement.vue')['default'] | ... | ... |
src/utils/qiniuFileHash.js
0 → 100644
| 1 | +import Rusha from 'rusha' | ||
| 2 | +/** | ||
| 3 | + * 完全遵循七牛云对象存储(Qiniu Kodo)的 ETag 计算规则,在浏览器中计算文件 ETag。 | ||
| 4 | + * | ||
| 5 | + * @param {File} file 用户通过 <input> 选择的 File 对象 | ||
| 6 | + * @param {(progress: { computed: number, total: number, percent: number }) => void} [progressCallback] 计算进度的回调函数 | ||
| 7 | + * @returns {Promise<string>} 一个 Promise,最终解析为计算出的 eTag 字符串 | ||
| 8 | + */ | ||
| 9 | +export const qiniuFileHash = async(file, progressCallback) => { | ||
| 10 | + // --- 辅助函数 --- | ||
| 11 | + | ||
| 12 | + // 1. SHA-1 计算 | ||
| 13 | + const createSha1 = window.crypto && window.crypto.subtle | ||
| 14 | + ? (data) => window.crypto.subtle.digest('SHA-1', data) // 使用 Web Crypto API | ||
| 15 | + : (data) => Rusha.createHash().update(data).digest(); // https://github.com/srijs/rusha | ||
| 16 | + | ||
| 17 | + // 2. 拼接 ArrayBuffer | ||
| 18 | + const concatArrayBuffers = (buffers) => { | ||
| 19 | + const totalLength = buffers.reduce((acc, b) => acc + b.byteLength, 0); | ||
| 20 | + const result = new Uint8Array(totalLength); | ||
| 21 | + let offset = 0; | ||
| 22 | + for (const buffer of buffers) { | ||
| 23 | + result.set(new Uint8Array(buffer), offset); | ||
| 24 | + offset += buffer.byteLength; | ||
| 25 | + } | ||
| 26 | + return result.buffer; | ||
| 27 | + }; | ||
| 28 | + | ||
| 29 | + // 3. ArrayBuffer 到 URL 安全 Base64 的转换 | ||
| 30 | + // 使用一个更健壮的 Base64 编码函数,避免大文件时出现栈溢出 | ||
| 31 | + const urlSafeBase64Encode = (buffer) => { | ||
| 32 | + let binary = ''; | ||
| 33 | + const bytes = new Uint8Array(buffer); | ||
| 34 | + // 对于小数据量 (21字节),这种方式性能足够,且比 fromCharCode.apply 更安全 | ||
| 35 | + for (let i = 0; i < bytes.byteLength; i++) { | ||
| 36 | + binary += String.fromCharCode(bytes[i]); | ||
| 37 | + } | ||
| 38 | + return btoa(binary).replace(/\//g, '_').replace(/\+/g, '-'); | ||
| 39 | + }; | ||
| 40 | + | ||
| 41 | + // 4. 带节流的进度回调包装器 | ||
| 42 | + let lastUpdateTime = 0; | ||
| 43 | + const throttleInterval = 100; // 每 100ms 更新一次进度 | ||
| 44 | + | ||
| 45 | + const throttledProgressCallback = (progress) => { | ||
| 46 | + if (!progressCallback) return; | ||
| 47 | + | ||
| 48 | + const now = Date.now(); | ||
| 49 | + // 对于最后 100% 的进度,我们总是希望它被立即调用,以确保状态最终正确。 | ||
| 50 | + if (progress.percent === 100) { | ||
| 51 | + setTimeout(() => progressCallback(progress), 0); | ||
| 52 | + return; | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + if (now - lastUpdateTime > throttleInterval) { | ||
| 56 | + lastUpdateTime = now; | ||
| 57 | + // 使用 setTimeout 解耦,防止阻塞 | ||
| 58 | + setTimeout(() => progressCallback(progress), 0); | ||
| 59 | + } | ||
| 60 | + }; | ||
| 61 | + | ||
| 62 | + // --- 主逻辑 --- | ||
| 63 | + | ||
| 64 | + if (file.size === 0) { | ||
| 65 | + throttledProgressCallback({ computed: 0, total: 0, percent: 100 }); | ||
| 66 | + return 'Fto5o-5ea0sNMlW_75VgGJCv2AcJ'; | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + const blockSize = 4 * 1024 * 1024; // 4MB | ||
| 70 | + | ||
| 71 | + // --- 1. 小文件处理 (小于等于4MB) --- | ||
| 72 | + if (file.size <= blockSize) { | ||
| 73 | + const fileBuffer = await file.arrayBuffer(); | ||
| 74 | + const sha1Buffer = await createSha1(fileBuffer); | ||
| 75 | + const prefix = new Uint8Array([0x16]); | ||
| 76 | + const finalBuffer = concatArrayBuffers([prefix.buffer, sha1Buffer]); | ||
| 77 | + const hash = urlSafeBase64Encode(finalBuffer); | ||
| 78 | + throttledProgressCallback({ computed: file.size, total: file.size, percent: 100 }); | ||
| 79 | + return hash; | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + // --- 2. 大文件处理 (需要分块) --- | ||
| 83 | + const sha1Results = []; // 存放每个完整块的 SHA-1 结果 | ||
| 84 | + let computed = 0; | ||
| 85 | + | ||
| 86 | + // 尝试使用高性能的 BYOB 流模式 | ||
| 87 | + let reader; | ||
| 88 | + try { | ||
| 89 | + const stream = file.stream(); | ||
| 90 | + reader = stream.getReader({ mode: 'byob' }); | ||
| 91 | + } catch (error) { | ||
| 92 | + // console.warn("BYOB reader not supported, falling back to slice() mode.", error); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + if (reader) { | ||
| 96 | + // --- 2a. 高性能 BYOB 流模式 --- | ||
| 97 | + let buffer = new Uint8Array(blockSize); // 我们需要一个缓冲区来累积数据,直到达到 4MB | ||
| 98 | + let offset = 0; // 当前缓冲区已填充的数据量 | ||
| 99 | + | ||
| 100 | + while (true) { | ||
| 101 | + // BYOB 读取器需要一个视图 (view) 来写入数据 | ||
| 102 | + // 我们让它写入到我们累积缓冲区的剩余空间 | ||
| 103 | + // 每次都基于当前 buffer 和 offset 创建 view | ||
| 104 | + const view = new Uint8Array(buffer.buffer, offset, buffer.byteLength - offset); | ||
| 105 | + const { done, value } = await reader.read(view); | ||
| 106 | + | ||
| 107 | + // 恢复对 buffer 的引用,因为 read() 后它可能被转移 (detached) | ||
| 108 | + buffer = new Uint8Array(value.buffer); | ||
| 109 | + | ||
| 110 | + if (done) { | ||
| 111 | + // 文件读取完毕,处理最后一个不满 4MB 的块 | ||
| 112 | + if (offset > 0) { | ||
| 113 | + const finalChunkView = new Uint8Array(buffer.buffer, 0, offset); | ||
| 114 | + const chunkSha1 = await createSha1(finalChunkView); | ||
| 115 | + // 最后一次计算文件分片时,不给计算进度,在所有计算都完成后,在给出计算进度 | ||
| 116 | + computed += finalChunkView.byteLength; | ||
| 117 | + sha1Results.push(chunkSha1); | ||
| 118 | + } | ||
| 119 | + break; | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + // 更新偏移量,value.byteLength 是本次实际读取到的字节数 | ||
| 123 | + offset += value.byteLength; | ||
| 124 | + | ||
| 125 | + // 检查缓冲区是否已满 | ||
| 126 | + if (offset === blockSize) { | ||
| 127 | + // 缓冲区满了,计算整个块的 SHA-1 | ||
| 128 | + const chunkSha1 = await createSha1(buffer); | ||
| 129 | + computed += buffer.byteLength; | ||
| 130 | + throttledProgressCallback({ computed: computed, total: file.size, percent: computed / file.size * 100 }); | ||
| 131 | + sha1Results.push(chunkSha1); | ||
| 132 | + offset = 0; // 重置偏移,复用 buffer | ||
| 133 | + } | ||
| 134 | + } | ||
| 135 | + reader.releaseLock(); // 释放流的锁 | ||
| 136 | + } | ||
| 137 | + else { | ||
| 138 | + // --- 2b. 回退到 slice() 并行模式 --- | ||
| 139 | + const blockCount = Math.ceil(file.size / blockSize); | ||
| 140 | + const promises = []; | ||
| 141 | + | ||
| 142 | + for (let i = 0; i < blockCount; i++) { | ||
| 143 | + const start = i * blockSize; | ||
| 144 | + const end = Math.min(start + blockSize, file.size); | ||
| 145 | + const chunk = file.slice(start, end); | ||
| 146 | + | ||
| 147 | + const promise = (async () => { | ||
| 148 | + const buffer = await chunk.arrayBuffer(); | ||
| 149 | + const chunkSha1 = await createSha1(buffer); | ||
| 150 | + computed += buffer.byteLength; | ||
| 151 | + if (computed < file.size) { | ||
| 152 | + // 最后一次计算文件分片时,不给计算进度,在所有计算都完成后,在给出计算进度 | ||
| 153 | + throttledProgressCallback({ computed: computed, total: file.size, percent: computed / file.size * 100 }); | ||
| 154 | + } | ||
| 155 | + return chunkSha1; | ||
| 156 | + })(); | ||
| 157 | + promises.push(promise); | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + // 使用 Promise.all 并行执行所有块的读取和 SHA-1 计算 | ||
| 161 | + const resolvedSha1s = await Promise.all(promises); | ||
| 162 | + sha1Results.push(...resolvedSha1s); | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + // --- 3. 最终计算 --- | ||
| 166 | + // 所有块的 SHA-1 计算完毕,进行最终的合并计算 | ||
| 167 | + const concatenatedSha1s = concatArrayBuffers(sha1Results); | ||
| 168 | + const finalSha1 = await createSha1(concatenatedSha1s); | ||
| 169 | + const prefix = new Uint8Array([0x96]); | ||
| 170 | + const finalBuffer = concatArrayBuffers([prefix.buffer, finalSha1]); | ||
| 171 | + | ||
| 172 | + const hash = urlSafeBase64Encode(finalBuffer); | ||
| 173 | + throttledProgressCallback({ computed: file.size, total: file.size, percent: 100 }); | ||
| 174 | + return hash; | ||
| 175 | +} |
| 1 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'; | 1 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'; |
| 2 | -import BMF from 'browser-md5-file'; | 2 | +import { qiniuFileHash } from '@/utils/qiniuFileHash'; |
| 3 | // import { v4 as uuidv4 } from 'uuid'; | 3 | // import { v4 as uuidv4 } from 'uuid'; |
| 4 | 4 | ||
| 5 | // 获取文件后缀 | 5 | // 获取文件后缀 |
| ... | @@ -7,19 +7,15 @@ const getFileSuffix = (fileName) => { | ... | @@ -7,19 +7,15 @@ const getFileSuffix = (fileName) => { |
| 7 | return /.[^.]+$/.exec(fileName) || ''; | 7 | return /.[^.]+$/.exec(fileName) || ''; |
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | -// 获取文件MD5 | 10 | +/** |
| 11 | -const getFileMD5 = (file) => { | 11 | + * 获取文件哈希(与七牛云ETag一致) |
| 12 | - return new Promise((resolve, reject) => { | 12 | + * @param {File} file 文件对象 |
| 13 | - const bmf = new BMF(); | 13 | + * @returns {Promise<string>} 哈希字符串 |
| 14 | - bmf.md5(file, (err, md5) => { | 14 | + * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。 |
| 15 | - if (err) { | 15 | + */ |
| 16 | - reject(err); | 16 | +const getFileMD5 = async (file) => { |
| 17 | - return; | 17 | + return await qiniuFileHash(file) |
| 18 | - } | 18 | +} |
| 19 | - resolve(md5); | ||
| 20 | - }); | ||
| 21 | - }); | ||
| 22 | -}; | ||
| 23 | 19 | ||
| 24 | // 上传文件到七牛云 | 20 | // 上传文件到七牛云 |
| 25 | const uploadToQiniu = async (file, token, fileName, onProgress) => { | 21 | const uploadToQiniu = async (file, token, fileName, onProgress) => { | ... | ... |
| ... | @@ -72,7 +72,7 @@ import { useRoute, useRouter } from 'vue-router' | ... | @@ -72,7 +72,7 @@ import { useRoute, useRouter } from 'vue-router' |
| 72 | import { showToast, showLoadingToast } from 'vant' | 72 | import { showToast, showLoadingToast } from 'vant' |
| 73 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common' | 73 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common' |
| 74 | import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin"; | 74 | import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin"; |
| 75 | -import BMF from 'browser-md5-file' | 75 | +import { qiniuFileHash } from '@/utils/qiniuFileHash'; |
| 76 | import _ from 'lodash' | 76 | import _ from 'lodash' |
| 77 | import { useTitle } from '@vueuse/core'; | 77 | import { useTitle } from '@vueuse/core'; |
| 78 | import { useAuth } from '@/contexts/auth' | 78 | import { useAuth } from '@/contexts/auth' |
| ... | @@ -134,18 +134,14 @@ const beforeRead = (file) => { | ... | @@ -134,18 +134,14 @@ const beforeRead = (file) => { |
| 134 | return flag | 134 | return flag |
| 135 | } | 135 | } |
| 136 | 136 | ||
| 137 | -// 获取文件MD5 | 137 | +/** |
| 138 | -const getFileMD5 = (file) => { | 138 | + * 获取文件哈希(与七牛云ETag一致) |
| 139 | - return new Promise((resolve, reject) => { | 139 | + * @param {File} file 文件对象 |
| 140 | - const bmf = new BMF() | 140 | + * @returns {Promise<string>} 哈希字符串 |
| 141 | - bmf.md5(file, (err, md5) => { | 141 | + * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。 |
| 142 | - if (err) { | 142 | + */ |
| 143 | - reject(err) | 143 | +const getFileMD5 = async (file) => { |
| 144 | - return | 144 | + return await qiniuFileHash(file) |
| 145 | - } | ||
| 146 | - resolve(md5) | ||
| 147 | - }) | ||
| 148 | - }) | ||
| 149 | } | 145 | } |
| 150 | 146 | ||
| 151 | // 上传到七牛云 | 147 | // 上传到七牛云 | ... | ... |
| ... | @@ -58,7 +58,7 @@ import { useRoute, useRouter } from 'vue-router' | ... | @@ -58,7 +58,7 @@ import { useRoute, useRouter } from 'vue-router' |
| 58 | import { showToast, showLoadingToast } from 'vant' | 58 | import { showToast, showLoadingToast } from 'vant' |
| 59 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common' | 59 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common' |
| 60 | import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin"; | 60 | import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin"; |
| 61 | -import BMF from 'browser-md5-file' | 61 | +import { qiniuFileHash } from '@/utils/qiniuFileHash'; |
| 62 | import _ from 'lodash' | 62 | import _ from 'lodash' |
| 63 | import { useTitle } from '@vueuse/core'; | 63 | import { useTitle } from '@vueuse/core'; |
| 64 | import { useAuth } from '@/contexts/auth' | 64 | import { useAuth } from '@/contexts/auth' |
| ... | @@ -130,18 +130,14 @@ const beforeRead = (file) => { | ... | @@ -130,18 +130,14 @@ const beforeRead = (file) => { |
| 130 | return flag | 130 | return flag |
| 131 | } | 131 | } |
| 132 | 132 | ||
| 133 | -// 获取文件MD5 | 133 | +/** |
| 134 | -const getFileMD5 = (file) => { | 134 | + * 获取文件哈希(与七牛云ETag一致) |
| 135 | - return new Promise((resolve, reject) => { | 135 | + * @param {File} file 文件对象 |
| 136 | - const bmf = new BMF() | 136 | + * @returns {Promise<string>} 哈希字符串 |
| 137 | - bmf.md5(file, (err, md5) => { | 137 | + * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。 |
| 138 | - if (err) { | 138 | + */ |
| 139 | - reject(err) | 139 | +const getFileMD5 = async (file) => { |
| 140 | - return | 140 | + return await qiniuFileHash(file) |
| 141 | - } | ||
| 142 | - resolve(md5) | ||
| 143 | - }) | ||
| 144 | - }) | ||
| 145 | } | 141 | } |
| 146 | 142 | ||
| 147 | // 上传到七牛云 | 143 | // 上传到七牛云 | ... | ... |
| ... | @@ -72,7 +72,7 @@ import { useRoute, useRouter } from 'vue-router' | ... | @@ -72,7 +72,7 @@ import { useRoute, useRouter } from 'vue-router' |
| 72 | import { showToast, showLoadingToast } from 'vant' | 72 | import { showToast, showLoadingToast } from 'vant' |
| 73 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common' | 73 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common' |
| 74 | import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin"; | 74 | import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin"; |
| 75 | -import BMF from 'browser-md5-file' | 75 | +import { qiniuFileHash } from '@/utils/qiniuFileHash'; |
| 76 | import _ from 'lodash' | 76 | import _ from 'lodash' |
| 77 | import { useTitle } from '@vueuse/core'; | 77 | import { useTitle } from '@vueuse/core'; |
| 78 | import { useAuth } from '@/contexts/auth' | 78 | import { useAuth } from '@/contexts/auth' |
| ... | @@ -134,18 +134,14 @@ const beforeRead = (file) => { | ... | @@ -134,18 +134,14 @@ const beforeRead = (file) => { |
| 134 | return flag | 134 | return flag |
| 135 | } | 135 | } |
| 136 | 136 | ||
| 137 | -// 获取文件MD5 | 137 | +/** |
| 138 | -const getFileMD5 = (file) => { | 138 | + * 获取文件哈希(与七牛云ETag一致) |
| 139 | - return new Promise((resolve, reject) => { | 139 | + * @param {File} file 文件对象 |
| 140 | - const bmf = new BMF() | 140 | + * @returns {Promise<string>} 哈希字符串 |
| 141 | - bmf.md5(file, (err, md5) => { | 141 | + * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。 |
| 142 | - if (err) { | 142 | + */ |
| 143 | - reject(err) | 143 | +const getFileMD5 = async (file) => { |
| 144 | - return | 144 | + return await qiniuFileHash(file) |
| 145 | - } | ||
| 146 | - resolve(md5) | ||
| 147 | - }) | ||
| 148 | - }) | ||
| 149 | } | 145 | } |
| 150 | 146 | ||
| 151 | // 上传到七牛云 | 147 | // 上传到七牛云 | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-03-24 13:04:21 | 2 | * @Date: 2025-03-24 13:04:21 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-06-09 11:47:32 | 4 | + * @LastEditTime: 2025-11-11 17:09:16 |
| 5 | * @FilePath: /mlaj/src/views/profile/settings/AvatarSettingPage.vue | 5 | * @FilePath: /mlaj/src/views/profile/settings/AvatarSettingPage.vue |
| 6 | * @Description: 修改头像页面 | 6 | * @Description: 修改头像页面 |
| 7 | --> | 7 | --> |
| ... | @@ -57,7 +57,7 @@ import FrostedGlass from "@/components/ui/FrostedGlass.vue"; | ... | @@ -57,7 +57,7 @@ import FrostedGlass from "@/components/ui/FrostedGlass.vue"; |
| 57 | import { getUserInfoAPI, updateUserInfoAPI } from "@/api/users"; | 57 | import { getUserInfoAPI, updateUserInfoAPI } from "@/api/users"; |
| 58 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'; | 58 | import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'; |
| 59 | import { showToast, showLoadingToast } from 'vant'; | 59 | import { showToast, showLoadingToast } from 'vant'; |
| 60 | -import BMF from 'browser-md5-file'; | 60 | +import { qiniuFileHash } from '@/utils/qiniuFileHash'; |
| 61 | import { useTitle } from '@vueuse/core'; | 61 | import { useTitle } from '@vueuse/core'; |
| 62 | import { useAuth } from '@/contexts/auth'; | 62 | import { useAuth } from '@/contexts/auth'; |
| 63 | 63 | ||
| ... | @@ -80,18 +80,14 @@ onMounted(async () => { | ... | @@ -80,18 +80,14 @@ onMounted(async () => { |
| 80 | } | 80 | } |
| 81 | }); | 81 | }); |
| 82 | 82 | ||
| 83 | -// 获取文件MD5 | 83 | +/** |
| 84 | -const getFileMD5 = (file) => { | 84 | + * 获取文件哈希(与七牛云ETag一致) |
| 85 | - return new Promise((resolve, reject) => { | 85 | + * @param {File} file 文件对象 |
| 86 | - const bmf = new BMF() | 86 | + * @returns {Promise<string>} 哈希字符串 |
| 87 | - bmf.md5(file, (err, md5) => { | 87 | + * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。 |
| 88 | - if (err) { | 88 | + */ |
| 89 | - reject(err) | 89 | +const getFileMD5 = async (file) => { |
| 90 | - return | 90 | + return await qiniuFileHash(file) |
| 91 | - } | ||
| 92 | - resolve(md5) | ||
| 93 | - }) | ||
| 94 | - }) | ||
| 95 | } | 91 | } |
| 96 | 92 | ||
| 97 | // 上传到七牛云 | 93 | // 上传到七牛云 | ... | ... |
| ... | @@ -2364,6 +2364,11 @@ run-parallel@^1.1.9: | ... | @@ -2364,6 +2364,11 @@ run-parallel@^1.1.9: |
| 2364 | dependencies: | 2364 | dependencies: |
| 2365 | queue-microtask "^1.2.2" | 2365 | queue-microtask "^1.2.2" |
| 2366 | 2366 | ||
| 2367 | +rusha@^0.8.14: | ||
| 2368 | + version "0.8.14" | ||
| 2369 | + resolved "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz#a977d0de9428406138b7bb90d3de5dcd024e2f68" | ||
| 2370 | + integrity sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA== | ||
| 2371 | + | ||
| 2367 | rust-result@^1.0.0: | 2372 | rust-result@^1.0.0: |
| 2368 | version "1.0.0" | 2373 | version "1.0.0" |
| 2369 | resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72" | 2374 | resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72" | ... | ... |
-
Please register or login to post a comment