diff --git a/frontend/src/pages/admin/versions/[id].vue b/frontend/src/pages/admin/versions/[id].vue index 745c93f..0d5d2d4 100644 --- a/frontend/src/pages/admin/versions/[id].vue +++ b/frontend/src/pages/admin/versions/[id].vue @@ -37,6 +37,8 @@ const versionId = computed(() => route.params.id as string) const loading = ref(true) const saving = ref(false) const uploading = ref(false) +const uploadProgress = ref(0) +const uploadSpeed = ref('') const applications = ref([]) const formData = ref({ @@ -168,11 +170,20 @@ async function uploadZipFile() { } uploading.value = true + uploadProgress.value = 0 + uploadSpeed.value = '' try { const formDataObj = new FormData() formDataObj.append('file', formData.value.file) - const response = await api.postFormData('/dev/versions/upload-zip', formDataObj) + const response = await api.postFormDataWithProgress( + '/dev/versions/upload-zip', + formDataObj, + (progress, speed) => { + uploadProgress.value = progress + uploadSpeed.value = speed + } + ) uploadedData.value = response toast.success(t('admin.versions.createForm.uploadSuccess')) } @@ -182,6 +193,8 @@ async function uploadZipFile() { } finally { uploading.value = false + uploadProgress.value = 0 + uploadSpeed.value = '' } } @@ -380,13 +393,26 @@ onMounted(() => { > - {{ t('admin.versions.createForm.uploadAndParse') }} + {{ uploading ? `${uploadProgress}%` : t('admin.versions.createForm.uploadAndParse') }} + +
+
+
+
+
+ {{ t('admin.versions.createForm.uploading') }} + {{ uploadSpeed }} +
+
diff --git a/frontend/src/pages/admin/versions/create.vue b/frontend/src/pages/admin/versions/create.vue index 309f59c..af36cc7 100644 --- a/frontend/src/pages/admin/versions/create.vue +++ b/frontend/src/pages/admin/versions/create.vue @@ -36,6 +36,8 @@ const router = useRouter() const loading = ref(false) const saving = ref(false) const uploading = ref(false) +const uploadProgress = ref(0) +const uploadSpeed = ref('') const applications = ref([]) const formData = ref({ @@ -149,11 +151,20 @@ async function uploadZipFile() { } uploading.value = true + uploadProgress.value = 0 + uploadSpeed.value = '' try { const formDataObj = new FormData() formDataObj.append('file', formData.value.file) - const response = await api.postFormData('/dev/versions/upload-zip', formDataObj) + const response = await api.postFormDataWithProgress( + '/dev/versions/upload-zip', + formDataObj, + (progress, speed) => { + uploadProgress.value = progress + uploadSpeed.value = speed + } + ) uploadedData.value = response toast.success(t('admin.versions.createForm.uploadSuccess')) } @@ -163,6 +174,8 @@ async function uploadZipFile() { } finally { uploading.value = false + uploadProgress.value = 0 + uploadSpeed.value = '' } } @@ -319,13 +332,26 @@ onMounted(() => { > - {{ t('admin.versions.createForm.uploadAndParse') }} + {{ uploading ? `${uploadProgress}%` : t('admin.versions.createForm.uploadAndParse') }}
+ +
+
+
+
+
+ {{ t('admin.versions.createForm.uploading') }} + {{ uploadSpeed }} +
+
diff --git a/frontend/src/plugins/i18n/en.json b/frontend/src/plugins/i18n/en.json index 766f992..f44c6c2 100644 --- a/frontend/src/plugins/i18n/en.json +++ b/frontend/src/plugins/i18n/en.json @@ -1535,6 +1535,7 @@ "zipOnly": "Only ZIP format files are supported", "selectZipFirst": "Please select a ZIP file first", "uploadSuccess": "File uploaded successfully", + "uploading": "Uploading...", "uploadFailed": "Upload failed", "selectAppRequired": "Please select an application", "versionRequired": "Please enter version number", diff --git a/frontend/src/plugins/i18n/zh.json b/frontend/src/plugins/i18n/zh.json index ef9b64a..5f9efcc 100644 --- a/frontend/src/plugins/i18n/zh.json +++ b/frontend/src/plugins/i18n/zh.json @@ -1513,6 +1513,7 @@ "zipOnly": "只支持ZIP格式文件", "selectZipFirst": "请先选择ZIP文件", "uploadSuccess": "文件上传成功", + "uploading": "正在上传...", "uploadFailed": "上传失败", "selectAppRequired": "请选择应用", "versionRequired": "请输入版本号", diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index d55b561..c65d3c4 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -67,6 +67,74 @@ const api = { return data.data }, + async postFormDataWithProgress( + endpoint: string, + formData: FormData, + onProgress?: (progress: number, speed: string) => void + ): Promise { + const token = localStorage.getItem('token') + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + + let startTime = Date.now() + let lastLoaded = 0 + + xhr.upload.addEventListener('progress', (event) => { + if (event.lengthComputable) { + const progress = Math.round((event.loaded / event.total) * 100) + + // 计算上传速度 + const currentTime = Date.now() + const elapsedTime = (currentTime - startTime) / 1000 // 秒 + const loadedSinceLast = event.loaded - lastLoaded + + if (elapsedTime > 0) { + const speed = loadedSinceLast / elapsedTime // bytes per second + const speedStr = formatSpeed(speed) + onProgress?.(progress, speedStr) + lastLoaded = event.loaded + } + } + }) + + xhr.addEventListener('load', () => { + if (xhr.status === 401) { + localStorage.removeItem('token') + localStorage.removeItem('user') + window.location.href = '/login' + reject(new Error('登录已过期,请重新登录')) + return + } + + try { + const data = JSON.parse(xhr.responseText) + if (xhr.status !== 200 || data.code !== 200) { + reject(new Error(data.message || '请求失败')) + } else { + resolve(data.data) + } + } catch { + reject(new Error('解析响应失败')) + } + }) + + xhr.addEventListener('error', () => { + reject(new Error('网络错误')) + }) + + xhr.addEventListener('abort', () => { + reject(new Error('上传已取消')) + }) + + xhr.open('POST', `${BASE_URL}${endpoint}`) + if (token) { + xhr.setRequestHeader('Authorization', `Bearer ${token}`) + } + xhr.send(formData) + }) + }, + async put(endpoint: string, body?: any): Promise { return this.request(endpoint, { method: 'PUT', @@ -82,5 +150,17 @@ const api = { }, } +function formatSpeed(bytesPerSecond: number): string { + if (bytesPerSecond < 1024) { + return `${bytesPerSecond.toFixed(0)} B/s` + } else if (bytesPerSecond < 1024 * 1024) { + return `${(bytesPerSecond / 1024).toFixed(2)} KB/s` + } else if (bytesPerSecond < 1024 * 1024 * 1024) { + return `${(bytesPerSecond / (1024 * 1024)).toFixed(2)} MB/s` + } else { + return `${(bytesPerSecond / (1024 * 1024 * 1024)).toFixed(2)} GB/s` + } +} + export { BASE_URL } export default api