diff --git a/app/api/performance/quick/route.ts b/app/api/performance/quick/route.ts
index 2d8a44c..66fdabf 100644
--- a/app/api/performance/quick/route.ts
+++ b/app/api/performance/quick/route.ts
@@ -154,6 +154,7 @@ export async function GET() {
uploadProcessing.push({
uploadId,
fileName: session.fileName,
+ fileSize: session.fileSize || 0,
progress,
userId: session.userId,
currentSpeed: (session as any).currentSpeed || 0, // 上传速度(字节/秒,估算)
diff --git a/components/Header.tsx b/components/Header.tsx
index 8023ae6..dcd25c8 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -11,6 +11,7 @@ import UpdateLogModal, { useUpdateLog, showUpdateLog } from '@/components/Update
import NetworkDiagnoseModal from '@/components/NetworkDiagnoseModal'
import MaintenanceBanner from '@/components/MaintenanceBanner'
import HeaderTaobaoSearch from '@/components/HeaderTaobaoSearch'
+import { formatHumanSpeed, formatTransferProgressSummary } from '@/lib/human-readable-transfer'
interface HeaderProps {
session: Session | null
@@ -252,18 +253,6 @@ export default function Header({ session }: HeaderProps) {
return name?.charAt(0).toUpperCase() || 'U'
}
- // 格式化速度(字节/秒 -> 用户友好单位)
- // 使用字节单位(B/s、KB/s、MB/s),而非比特单位
- const formatSpeed = (bytesPerSecond: number): string => {
- if (bytesPerSecond < 1024) {
- return `${bytesPerSecond.toFixed(0)} B/s`
- }
- if (bytesPerSecond < 1024 * 1024) {
- return `${(bytesPerSecond / 1024).toFixed(2)} KB/s`
- }
- return `${(bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s`
- }
-
const formatAgo = (ms: number): string => {
if (!Number.isFinite(ms) || ms < 0) return '--'
const sec = Math.floor(ms / 1000)
@@ -382,28 +371,32 @@ export default function Header({ session }: HeaderProps) {
- {/* 网络带宽 */}
+ {/* 服务器整体网络流量 */}
- {perfData.uploadQueue.processing.map((task: any, idx: number) => (
-
-
{task.fileName || '未知文件'}
-
- {task.username || '未知用户'}
- {task.materialType || '未知类型'}
-
-
- 进度: {task.progress ?? 0}%
-
- {task.currentSpeed ? `速度: ${formatSpeed(task.currentSpeed)}` : '速度: --'}
-
-
- {task.isActive === false && (
-
- 无活动: {formatAgo(task.inactiveMs)}
+ {perfData.uploadQueue.processing.map((task: any, idx: number) => {
+ const uploadedBytes =
+ task.uploadedBytes || ((task.fileSize || 0) * ((task.progress || 0) / 100))
+ const speedText = task.currentSpeed
+ ? formatTransferProgressSummary({
+ speedBytesPerSecond: task.currentSpeed,
+ loadedBytes: uploadedBytes,
+ totalBytes: task.fileSize,
+ })
+ : '速度: --'
+
+ return (
+
+
{task.fileName || '未知文件'}
+
+ {task.username || '未知用户'}
+ {task.materialType || '未知类型'}
- )}
-
- ))}
+
+
进度: {task.progress ?? 0}%
+
{speedText}
+
+ {task.isActive === false && (
+
+ 无活动: {formatAgo(task.inactiveMs)}
+
+ )}
+
+ )
+ })}
)}
{perfData?.uploadQueue?.waiting && perfData.uploadQueue.waiting.length > 0 && (
diff --git a/components/NetworkDiagnoseModal.tsx b/components/NetworkDiagnoseModal.tsx
index 21e580c..25862b9 100644
--- a/components/NetworkDiagnoseModal.tsx
+++ b/components/NetworkDiagnoseModal.tsx
@@ -3,6 +3,7 @@
import { useState, useRef, useEffect } from 'react'
import { useToast } from '@/hooks/useToast'
import ToastBubble from './ToastBubble'
+import { formatHumanSpeed } from '@/lib/human-readable-transfer'
interface NetworkDiagnoseModalProps {
isOpen: boolean
@@ -84,18 +85,6 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
}
}, [])
- // 格式化速度(字节/秒 -> 用户友好单位)
- // 使用字节单位(B/s、KB/s、MB/s),与其他地方保持一致
- const formatSpeed = (bytesPerSecond: number) => {
- if (bytesPerSecond < 1024) {
- return `${bytesPerSecond.toFixed(0)} B/s`
- }
- if (bytesPerSecond < 1024 * 1024) {
- return `${(bytesPerSecond / 1024).toFixed(2)} KB/s`
- }
- return `${(bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s`
- }
-
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`
@@ -166,13 +155,16 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
const downlink = connection?.downlink || 'unknown'
const rtt = connection?.rtt || 'unknown'
const saveData = connection?.saveData || false
+ const browserDownlinkSpeed = typeof downlink === 'number'
+ ? formatHumanSpeed((downlink * 1000 * 1000) / 8)
+ : '未知'
const message = [
`网络类型: ${data.isIntranet ? '内网' : '外网'}`,
`访问域名: ${data.hostname}`,
`客户端IP: ${data.clientIp}`,
`浏览器网络: ${networkType}`,
- `下行速度: ${downlink !== 'unknown' ? `${(downlink / 8).toFixed(2)} MB/s` : '未知'}`,
+ `浏览器估算下行: ${browserDownlinkSpeed}`,
`RTT延迟: ${rtt !== 'unknown' ? `${rtt} ms` : '未知'}`,
saveData ? '省流模式: 开启' : ''
].filter(Boolean).join(', ')
@@ -300,7 +292,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
type: 'download',
status: speed < 1024 * 1024 ? 'warning' : 'success', // < 1MB/s 警告
speed,
- message: `下载速度: ${formatSpeed(speed)}${ttfb > 0 ? `, TTFB: ${ttfb.toFixed(0)}ms` : ''}`,
+ message: `下载速度: ${formatHumanSpeed(speed)}${ttfb > 0 ? `, TTFB: ${ttfb.toFixed(0)}ms` : ''}`,
details: {
ttfb: ttfb > 0 ? ttfb : undefined,
totalTime: duration * 1000,
@@ -341,7 +333,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
type: 'upload',
status: speed < 512 * 1024 ? 'warning' : 'success', // < 512KB/s 警告
speed,
- message: `上传速度: ${formatSpeed(speed)}`,
+ message: `上传速度: ${formatHumanSpeed(speed)}`,
details: {
totalTime: duration * 1000,
fileSize: testData.size
@@ -448,7 +440,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
type: 'preview-image',
status: loadTime > 3000 ? 'warning' : 'success',
speed,
- message: `加载时间: ${(loadTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(${formatSpeed(speed)})` : ''}`,
+ message: `加载时间: ${(loadTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(${formatHumanSpeed(speed)})` : ''}`,
details: {
totalTime: loadTime,
ttfb: ttfb > 0 ? ttfb : undefined,
@@ -713,7 +705,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
type: 'proxy-performance',
status: ttfb > 1000 ? 'warning' : 'success',
speed,
- message: `代理响应: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatSpeed(speed)}`,
+ message: `代理响应: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`,
details: {
dnsTime: dnsTime > 0 ? dnsTime : undefined,
connectTime: connectTime > 0 ? connectTime : undefined,
@@ -782,7 +774,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
type: 'video-range',
status: rangeTime > 2000 ? 'warning' : 'success',
speed,
- message: `Range请求: ${rangeTime.toFixed(0)}ms, 速度 ${formatSpeed(speed)}`,
+ message: `Range请求: ${rangeTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`,
details: {
totalTime: rangeTime,
fileSize: blob.size,
@@ -862,7 +854,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
type: 'intranet-minio',
status: ttfb > 500 ? 'warning' : 'success',
speed,
- message: `内网MinIO: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatSpeed(speed)}`,
+ message: `内网MinIO: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`,
details: {
ttfb: ttfb > 0 ? ttfb : totalTime,
totalTime,
@@ -969,7 +961,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
type: 'public-s3',
status: ttfb > 2000 ? 'warning' : 'success',
speed,
- message: `公网S3: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatSpeed(speed)}`,
+ message: `公网S3: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`,
details: {
dnsTime: dnsTime > 0 ? dnsTime : undefined,
connectTime: connectTime > 0 ? connectTime : undefined,
@@ -1039,7 +1031,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
type: 'concurrent',
status: avgTimePerFile > 2000 ? 'warning' : 'success',
speed: avgSpeed,
- message: `并发${imageUrls.length}个文件: 平均${avgTimePerFile.toFixed(0)}ms/文件, 总速度 ${formatSpeed(avgSpeed)}`,
+ message: `并发${imageUrls.length}个文件: 平均${avgTimePerFile.toFixed(0)}ms/文件, 总速度 ${formatHumanSpeed(avgSpeed)}`,
details: {
totalTime,
totalSize,
@@ -1235,9 +1227,8 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
createdBlobs.current.push(blob) // 跟踪Blob以便清理
const totalTime = performance.now() - startTime
- const fileSizeMB = blob.size / (1024 * 1024)
const downloadTime = (totalTime - ttfb) / 1000
- const downloadSpeed = downloadTime > 0 ? fileSizeMB / downloadTime : 0
+ const downloadSpeed = downloadTime > 0 ? blob.size / downloadTime : 0
// 获取Resource Timing
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
@@ -1271,8 +1262,8 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
}
}
- if (downloadSpeed < 1 && downloadSpeed > 0) {
- issues.push(`🔴 下载速度过慢: ${downloadSpeed.toFixed(2)}MB/s(正常>10MB/s)`)
+ if (downloadSpeed < 1024 * 1024 && downloadSpeed > 0) {
+ issues.push(`🔴 下载速度偏慢: ${formatHumanSpeed(downloadSpeed)}(低于 ${formatHumanSpeed(1024 * 1024)})`)
if (!isFrp) {
suggestions.push('• 带宽不足,建议升级服务器带宽或使用CDN')
}
@@ -1286,12 +1277,12 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
issues.push(`⚠ TCP连接较慢: ${connectTime.toFixed(0)}ms`)
}
- const isSuccess = !isFrp && ttfb < 1000 && downloadSpeed > 1
- const isWarning = !isSuccess && (ttfb < 3000 || downloadSpeed > 0.1)
+ const isSuccess = !isFrp && ttfb < 1000 && downloadSpeed > 1024 * 1024
+ const isWarning = !isSuccess && (ttfb < 3000 || downloadSpeed > 100 * 1024)
let message = ''
if (isSuccess) {
- message = `性能良好\n下载速度: ${downloadSpeed.toFixed(2)}MB/s, TTFB: ${Math.round(ttfb)}ms`
+ message = `性能良好\n下载速度: ${formatHumanSpeed(downloadSpeed)}, TTFB: ${Math.round(ttfb)}ms`
} else if (isWarning) {
message = `性能一般\n${issues.slice(0, 2).join('\n')}`
} else {
@@ -2461,6 +2452,9 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
const downlink = connection?.downlink || 'unknown'
const rtt = connection?.rtt || 'unknown'
const saveData = connection?.saveData || false
+ const browserDownlinkSpeed = typeof downlink === 'number'
+ ? formatHumanSpeed((downlink * 1000 * 1000) / 8)
+ : '未知'
const report = [
'=== 网络诊断完整报告 ===',
@@ -2471,7 +2465,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
'=== 网络环境信息 ===',
`在线状态: ${navigator.onLine ? '在线' : '离线'}`,
`网络类型: ${networkType}`,
- `下行速度: ${downlink !== 'unknown' ? `${(downlink / 8).toFixed(2)} MB/s` : '未知'}`,
+ `浏览器估算下行: ${browserDownlinkSpeed}`,
`RTT延迟: ${rtt !== 'unknown' ? `${rtt} ms` : '未知'}`,
`省流模式: ${saveData ? '开启' : '关闭'}`,
networkInfo ? [
@@ -2533,7 +2527,7 @@ export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnos
if (r.details.responseTime !== undefined) details.push(`响应时间: ${r.details.responseTime.toFixed(0)}ms`)
if (r.details.totalTime !== undefined) details.push(`总耗时: ${r.details.totalTime.toFixed(0)}ms`)
if (r.details.fileSize !== undefined) details.push(`文件大小: ${formatFileSize(r.details.fileSize)}`)
- if (r.details.downloadSpeed !== undefined) details.push(`下载速度: ${r.details.downloadSpeed.toFixed(2)}MB/s`)
+ if (r.details.downloadSpeed !== undefined) details.push(`下载速度: ${formatHumanSpeed(r.details.downloadSpeed)}`)
if (r.details.url) details.push(`URL: ${r.details.url}`)
if (r.details.isFrp !== undefined) details.push(`FRP穿透: ${r.details.isFrp ? '是' : '否'}`)
if (r.details.supportsRange !== undefined) details.push(`Range支持: ${r.details.supportsRange ? '是' : '否'}`)
diff --git a/components/SkuEditModal.tsx b/components/SkuEditModal.tsx
index 075ec59..91c419a 100644
--- a/components/SkuEditModal.tsx
+++ b/components/SkuEditModal.tsx
@@ -26,7 +26,7 @@ import {
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { parseApiError, parseFetchError } from '@/lib/error-handler'
-import { formatSpeed } from '@/components/upload/upload-utils'
+import { formatTransferProgress } from '@/components/upload/upload-utils'
import {
STANDARD_MODEL_WHITE_BG_OTHER_FIELD_NAME,
STANDARD_MODEL_WHITE_BG_VIEWS,
@@ -110,6 +110,9 @@ export default function SkuEditModal({ materialId, isOpen, onClose, onSaved }: S
const [whiteBgUploadSpeed, setWhiteBgUploadSpeed] = useState(0)
const [whiteBgError, setWhiteBgError] = useState('')
const [whiteBgSourceType, setWhiteBgSourceType] = useState
('')
+ const whiteBgUploadTotalBytes =
+ STANDARD_MODEL_WHITE_BG_VIEWS.reduce((sum, view) => sum + (whiteBgFiles[view.key]?.size || 0), 0) +
+ whiteBgOtherImages.reduce((sum, image) => sum + image.file.size, 0)
const fileInputRef = useRef(null)
const previewInputRef = useRef(null)
@@ -1361,7 +1364,15 @@ export default function SkuEditModal({ materialId, isOpen, onClose, onSaved }: S
上传进度 {whiteBgUploadProgress}%
- {whiteBgUploadSpeed > 0 ? formatSpeed(whiteBgUploadSpeed) : '计算中...'}
+
+ {whiteBgUploadSpeed > 0
+ ? formatTransferProgress(
+ whiteBgUploadSpeed,
+ whiteBgUploadTotalBytes * (whiteBgUploadProgress / 100),
+ whiteBgUploadTotalBytes || undefined,
+ )
+ : '计算中...'}
+
>(new Set()) // 单文件上传的所有活跃XHR
const singleUploadServerStateRef = useRef<{ uploadId?: string; existingFilePath?: string } | null>(null)
const batchFileInputRef = useRef
(null) // 批量文件选择input
+ const singleUploadTotalBytes =
+ (zipFile?.size || 0) +
+ previewImages.reduce((sum, file) => sum + file.size, 0) +
+ (displayImage?.size || 0) +
+ modelImages.reduce((sum, file) => sum + file.size, 0)
// 加载当前用户可见的素材类型
useEffect(() => {
@@ -2334,6 +2337,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
batchFiles={batchFiles}
uploadProgress={uploadProgress}
uploadSpeed={uploadSpeed}
+ uploadTotalBytes={singleUploadTotalBytes || undefined}
isProcessing={isProcessing}
onCancel={handleCancelUpload}
/>
diff --git a/components/upload/UploadProgress.tsx b/components/upload/UploadProgress.tsx
index 4f1637d..afb1dda 100644
--- a/components/upload/UploadProgress.tsx
+++ b/components/upload/UploadProgress.tsx
@@ -1,7 +1,7 @@
'use client'
import type { BatchUploadStatus, BatchFileItem } from './upload-types'
-import { formatSpeed, calculateRemainingTime } from './upload-utils'
+import { formatTransferProgress } from './upload-utils'
interface UploadProgressProps {
uploading: boolean
@@ -10,6 +10,7 @@ interface UploadProgressProps {
batchFiles?: BatchFileItem[] // 用于计算实际文件大小
uploadProgress: number
uploadSpeed: number
+ uploadTotalBytes?: number
isProcessing: boolean
onCancel: () => void
}
@@ -21,6 +22,7 @@ export default function UploadProgress({
batchFiles,
uploadProgress,
uploadSpeed,
+ uploadTotalBytes,
isProcessing,
onCancel,
}: UploadProgressProps) {
@@ -31,24 +33,13 @@ export default function UploadProgress({
const completedFiles = fileStatuses.filter(s => s.status === 'done').length
const failedFiles = fileStatuses.filter(s => s.status === 'failed').length
const processingFiles = fileStatuses.filter(s => s.status === 'uploading' || s.status === 'processing' || s.status === 'pending')
+ const currentBatchFile = batchFiles?.find(bf => bf.file.name === currentFileName)
+ const currentFileTotalBytes = currentBatchFile?.file.size || 0
+ const currentFileLoadedBytes = currentFileTotalBytes * (currentFileProgress / 100)
// 只显示正在处理/等待中的文件,已完成的文件不显示(避免队列过长)
const visibleFileStatuses = fileStatuses.filter(s => s.status !== 'done')
- // 计算剩余字节数(使用实际文件大小)
- const remainingBytes = fileStatuses
- .slice(currentFileIndex)
- .reduce((sum, status, idx) => {
- const fileStatus = batchUploadStatus.fileStatuses[currentFileIndex + idx]
- if (!fileStatus) return sum
-
- // 从batchFiles中查找对应的文件大小
- const batchFile = batchFiles?.find(bf => bf.file.name === fileStatus.fileName)
- const fileSize = batchFile?.file.size || 0
-
- return sum + (fileSize * (100 - status.progress) / 100)
- }, 0)
-
return (
{/* 整体进度 */}
@@ -83,9 +74,14 @@ export default function UploadProgress({
/>
{currentFileSpeed > 0 && (
-
-
速度: {formatSpeed(currentFileSpeed)}
-
剩余时间: {calculateRemainingTime(remainingBytes, currentFileSpeed)}
+
+
+ {formatTransferProgress(
+ currentFileSpeed,
+ currentFileLoadedBytes,
+ currentFileTotalBytes || undefined,
+ )}
+
)}
@@ -165,7 +161,11 @@ export default function UploadProgress({
{uploadSpeed > 0 && !isProcessing && (
- 速度: {formatSpeed(uploadSpeed)}
+ {formatTransferProgress(
+ uploadSpeed,
+ uploadTotalBytes ? uploadTotalBytes * (uploadProgress / 100) : undefined,
+ uploadTotalBytes,
+ )}
)}