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?.network?.uploadSpeed - ? formatSpeed(perfData.network.uploadSpeed) + ? formatHumanSpeed(perfData.network.uploadSpeed) : '--'}
- 下载 + 出站 {perfData?.network?.downloadSpeed - ? formatSpeed(perfData.network.downloadSpeed) + ? formatHumanSpeed(perfData.network.downloadSpeed) : '--'}
+
+ 服务器整体流量,非当前浏览器测速 +
@@ -422,26 +415,36 @@ export default function Header({ session }: HeaderProps) { {perfData?.uploadQueue?.processing && perfData.uploadQueue.processing.length > 0 && (
- {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, + )}
)}