feat(上传): 实现视频文件上传功能并添加文件校验
添加 `browser-md5-file` 依赖,实现视频文件上传功能,包括文件校验、上传进度跟踪及七牛云上传逻辑。移除不再使用的 `@uppy` 相关依赖。
Showing
4 changed files
with
146 additions
and
23 deletions
This diff is collapsed. Click to expand it.
| ... | @@ -19,6 +19,7 @@ | ... | @@ -19,6 +19,7 @@ |
| 19 | "@vant/touch-emulator": "^1.4.0", | 19 | "@vant/touch-emulator": "^1.4.0", |
| 20 | "@vant/use": "^1.6.0", | 20 | "@vant/use": "^1.6.0", |
| 21 | "@videojs-player/vue": "^1.0.0", | 21 | "@videojs-player/vue": "^1.0.0", |
| 22 | + "browser-md5-file": "^1.1.1", | ||
| 22 | "dayjs": "^1.11.13", | 23 | "dayjs": "^1.11.13", |
| 23 | "swiper": "^11.2.6", | 24 | "swiper": "^11.2.6", |
| 24 | "vant": "^4.9.18", | 25 | "vant": "^4.9.18", | ... | ... |
| ... | @@ -41,7 +41,7 @@ | ... | @@ -41,7 +41,7 @@ |
| 41 | import { ref, defineProps, defineEmits, watch } from 'vue'; | 41 | import { ref, defineProps, defineEmits, watch } from 'vue'; |
| 42 | import { showToast } from 'vant'; | 42 | import { showToast } from 'vant'; |
| 43 | import VideoPlayer from '@/components/ui/VideoPlayer.vue'; | 43 | import VideoPlayer from '@/components/ui/VideoPlayer.vue'; |
| 44 | -import { v4 as uuidv4 } from 'uuid'; | 44 | +import { uploadFile, validateFile } from '@/utils/upload'; |
| 45 | 45 | ||
| 46 | const props = defineProps({ | 46 | const props = defineProps({ |
| 47 | modelValue: { | 47 | modelValue: { |
| ... | @@ -76,8 +76,9 @@ const uploadProgress = ref(0); | ... | @@ -76,8 +76,9 @@ const uploadProgress = ref(0); |
| 76 | const maxSize = 100 * 1024 * 1024; // 100MB | 76 | const maxSize = 100 * 1024 * 1024; // 100MB |
| 77 | 77 | ||
| 78 | const beforeRead = (file) => { | 78 | const beforeRead = (file) => { |
| 79 | - if (!file.type.includes('video/')) { | 79 | + const validation = validateFile(file); |
| 80 | - showToast('请上传视频文件'); | 80 | + if (!validation.valid) { |
| 81 | + showToast(validation.message); | ||
| 81 | return false; | 82 | return false; |
| 82 | } | 83 | } |
| 83 | return true; | 84 | return true; |
| ... | @@ -90,28 +91,22 @@ const afterRead = async (file) => { | ... | @@ -90,28 +91,22 @@ const afterRead = async (file) => { |
| 90 | videoName.value = ''; | 91 | videoName.value = ''; |
| 91 | uploadProgress.value = 0; | 92 | uploadProgress.value = 0; |
| 92 | 93 | ||
| 93 | - const formData = new FormData(); | ||
| 94 | - formData.append('file', file.file); | ||
| 95 | - | ||
| 96 | try { | 94 | try { |
| 97 | - // 模拟上传进度 | 95 | + // 实际上传逻辑 |
| 98 | - const timer = setInterval(() => { | 96 | + const fileCode = 'video'; // 视频上传的fileCode |
| 99 | - uploadProgress.value += 10; | 97 | + const result = await uploadFile(file.file, fileCode, (progress) => { |
| 100 | - if (uploadProgress.value >= 100) { | 98 | + uploadProgress.value = progress; |
| 101 | - clearInterval(timer); | 99 | + }); |
| 102 | - // 模拟上传成功后的视频URL | 100 | + |
| 103 | - videoUrl.value = URL.createObjectURL(file.file); | 101 | + if (result && result.src) { |
| 104 | - videoId.value = uuidv4(); | 102 | + videoUrl.value = result.src; |
| 105 | - videoName.value = file.file.name; | 103 | + videoId.value = result.meta_id || result.hash; |
| 106 | - } | 104 | + videoName.value = file.file.name; |
| 107 | - }, 300); | 105 | + } else { |
| 108 | - | 106 | + throw new Error('上传失败'); |
| 109 | - // TODO: 实际的上传逻辑 | 107 | + } |
| 110 | - // const response = await uploadVideo(formData); | ||
| 111 | - // videoUrl.value = response.data.url; | ||
| 112 | - // videoId.value = uuidv4(); | ||
| 113 | - // videoName.value = file.file.name; | ||
| 114 | } catch (error) { | 108 | } catch (error) { |
| 109 | + uploadProgress.value = 0; | ||
| 115 | showToast('上传失败'); | 110 | showToast('上传失败'); |
| 116 | console.error('Upload error:', error); | 111 | console.error('Upload error:', error); |
| 117 | } | 112 | } | ... | ... |
src/utils/upload.js
0 → 100644
| 1 | +import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'; | ||
| 2 | +import BMF from 'browser-md5-file'; | ||
| 3 | +// import { v4 as uuidv4 } from 'uuid'; | ||
| 4 | + | ||
| 5 | +// 获取文件后缀 | ||
| 6 | +const getFileSuffix = (fileName) => { | ||
| 7 | + return /.[^.]+$/.exec(fileName) || ''; | ||
| 8 | +}; | ||
| 9 | + | ||
| 10 | +// 获取文件MD5 | ||
| 11 | +const getFileMD5 = (file) => { | ||
| 12 | + return new Promise((resolve, reject) => { | ||
| 13 | + const bmf = new BMF(); | ||
| 14 | + bmf.md5(file, (err, md5) => { | ||
| 15 | + if (err) { | ||
| 16 | + reject(err); | ||
| 17 | + return; | ||
| 18 | + } | ||
| 19 | + resolve(md5); | ||
| 20 | + }); | ||
| 21 | + }); | ||
| 22 | +}; | ||
| 23 | + | ||
| 24 | +// 上传文件到七牛云 | ||
| 25 | +const uploadToQiniu = async (file, token, fileName, onProgress) => { | ||
| 26 | + const formData = new FormData(); | ||
| 27 | + formData.append('file', file); | ||
| 28 | + formData.append('token', token); | ||
| 29 | + formData.append('key', fileName); | ||
| 30 | + | ||
| 31 | + const config = { | ||
| 32 | + headers: { 'Content-Type': 'multipart/form-data' }, | ||
| 33 | + onUploadProgress: (progressEvent) => { | ||
| 34 | + if (progressEvent.total > 0) { | ||
| 35 | + const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total); | ||
| 36 | + // 使用requestAnimationFrame确保进度更新的平滑性 | ||
| 37 | + requestAnimationFrame(() => { | ||
| 38 | + onProgress?.(percent); | ||
| 39 | + }); | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + }; | ||
| 43 | + | ||
| 44 | + // 根据协议选择上传地址 | ||
| 45 | + const qiniuUploadUrl = window.location.protocol === 'https:' | ||
| 46 | + ? 'https://up.qbox.me' | ||
| 47 | + : 'http://upload.qiniu.com'; | ||
| 48 | + | ||
| 49 | + return await qiniuUploadAPI(qiniuUploadUrl, formData, config); | ||
| 50 | +}; | ||
| 51 | + | ||
| 52 | +// 校验文件 | ||
| 53 | +export const validateFile = (file, options = {}) => { | ||
| 54 | + const { | ||
| 55 | + maxSize = 100, // 默认100MB | ||
| 56 | + allowedTypes = ['video/mp4', 'video/quicktime'], | ||
| 57 | + } = options; | ||
| 58 | + | ||
| 59 | + if (!allowedTypes.includes(file.type)) { | ||
| 60 | + return { | ||
| 61 | + valid: false, | ||
| 62 | + message: '请上传正确格式的视频文件' | ||
| 63 | + }; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + const fileSize = (file.size / 1024 / 1024).toFixed(2); | ||
| 67 | + if (fileSize > maxSize) { | ||
| 68 | + return { | ||
| 69 | + valid: false, | ||
| 70 | + message: `文件大小不能超过${maxSize}MB` | ||
| 71 | + }; | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + return { valid: true }; | ||
| 75 | +}; | ||
| 76 | + | ||
| 77 | +// 上传文件 | ||
| 78 | +export const uploadFile = async (file, fileCode, onProgress) => { | ||
| 79 | + try { | ||
| 80 | + // 获取文件MD5 | ||
| 81 | + const md5 = await getFileMD5(file); | ||
| 82 | + | ||
| 83 | + // 获取七牛token | ||
| 84 | + const tokenResult = await qiniuTokenAPI({ | ||
| 85 | + name: file.name, | ||
| 86 | + hash: md5 | ||
| 87 | + }); | ||
| 88 | + | ||
| 89 | + // 如果文件已存在,直接返回 | ||
| 90 | + if (tokenResult.data) { | ||
| 91 | + onProgress?.(100); | ||
| 92 | + return tokenResult.data; | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + // 新文件上传 | ||
| 96 | + if (tokenResult.token) { | ||
| 97 | + const suffix = getFileSuffix(file.name); | ||
| 98 | + const fileName = `uploadForm/${fileCode}/${md5}${suffix}`; | ||
| 99 | + | ||
| 100 | + // TODO: image_info 为七牛返回的图片信息,现在是上传视频看后期适配 | ||
| 101 | + const { filekey, image_info } = await uploadToQiniu( | ||
| 102 | + file, | ||
| 103 | + tokenResult.token, | ||
| 104 | + fileName, | ||
| 105 | + onProgress | ||
| 106 | + ); | ||
| 107 | + | ||
| 108 | + if (filekey) { | ||
| 109 | + // 保存文件信息 | ||
| 110 | + const { data } = await saveFileAPI({ | ||
| 111 | + name: file.name, | ||
| 112 | + filekey, | ||
| 113 | + hash: md5, | ||
| 114 | + height: image_info?.height, | ||
| 115 | + width: image_info?.width, | ||
| 116 | + }); | ||
| 117 | + | ||
| 118 | + return data; | ||
| 119 | + } | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + throw new Error('上传失败'); | ||
| 123 | + } catch (error) { | ||
| 124 | + console.error('Upload error:', error); | ||
| 125 | + throw error; | ||
| 126 | + } | ||
| 127 | +}; |
-
Please register or login to post a comment