'use client' import { useState } from 'react' import { useToast } from '@/hooks/useToast' import ToastBubble from './ToastBubble' interface NetworkDiagnoseModalProps { isOpen: boolean onClose: () => void } interface TestResult { type: 'download' | 'upload' | 'storage' | 'preview-image' | 'preview-video' | 'preview-audio' | 'dns' | 'connection' status: 'testing' | 'success' | 'error' | 'warning' speed?: number message?: string details?: { dnsTime?: number connectTime?: number ttfb?: number // Time To First Byte downloadTime?: number totalTime?: number fileSize?: number url?: string } } export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnoseModalProps) { const [testing, setTesting] = useState(false) const [results, setResults] = useState([]) const [copied, setCopied] = useState(false) const { toast, showToast } = useToast() // 格式化速度 const formatSpeed = (bytesPerSecond: number) => { if (bytesPerSecond < 1024) return `${bytesPerSecond.toFixed(2)} B/s` if (bytesPerSecond < 1024 * 1024) return `${(bytesPerSecond / 1024).toFixed(2)} KB/s` return `${(bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s` } // 下载测速 const testDownloadSpeed = async (): Promise => { try { const testFileSize = 1024 * 1024 // 1MB const startTime = Date.now() const response = await fetch('/api/network/test-download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ size: testFileSize }) }) if (!response.ok) throw new Error('下载测试失败') const blob = await response.blob() const endTime = Date.now() const duration = (endTime - startTime) / 1000 // 秒 const speed = blob.size / duration // 字节/秒 return { type: 'download', status: 'success', speed, message: `下载速度: ${formatSpeed(speed)}` } } catch (error) { return { type: 'download', status: 'error', message: '下载测试失败' } } } // 上传测速 const testUploadSpeed = async (): Promise => { try { const testData = new Blob([new ArrayBuffer(1024 * 1024)]) // 1MB const formData = new FormData() formData.append('file', testData, 'test.bin') const startTime = Date.now() const response = await fetch('/api/network/test-upload', { method: 'POST', body: formData }) if (!response.ok) throw new Error('上传测试失败') const endTime = Date.now() const duration = (endTime - startTime) / 1000 // 秒 const speed = testData.size / duration // 字节/秒 return { type: 'upload', status: 'success', speed, message: `上传速度: ${formatSpeed(speed)}` } } catch (error) { return { type: 'upload', status: 'error', message: '上传测试失败' } } } // 存储连通性测试 const testStorageConnectivity = async (): Promise => { try { const response = await fetch('/api/network/test-storage') if (!response.ok) throw new Error('存储连通性测试失败') const data = await response.json() return { type: 'storage', status: 'success', message: data.message || '存储服务连接正常' } } catch (error) { return { type: 'storage', status: 'error', message: '存储服务连接失败' } } } // 预览图片加载测试 const testPreviewImageLoad = async (): Promise => { try { // 获取一个真实的预览图片URL const response = await fetch('/api/materials?pageSize=1&materialType=3D场景') if (!response.ok) throw new Error('获取素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'preview-image', status: 'warning', message: '没有可测试的预览图片' } } const imageUrl = materials[0].previewUrl if (!imageUrl) { return { type: 'preview-image', status: 'warning', message: '素材没有预览图' } } // 使用Performance API测量加载时间 const startTime = performance.now() const img = new Image() await new Promise((resolve, reject) => { img.onload = resolve img.onerror = reject img.src = imageUrl }) const endTime = performance.now() const loadTime = endTime - startTime // 估算文件大小(通过fetch获取) const sizeResponse = await fetch(imageUrl, { method: 'HEAD' }) const fileSize = parseInt(sizeResponse.headers.get('content-length') || '0') const speed = fileSize > 0 ? (fileSize / (loadTime / 1000)) : 0 return { type: 'preview-image', status: loadTime > 3000 ? 'warning' : 'success', speed, message: `加载时间: ${(loadTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(${formatSpeed(speed)})` : ''}`, details: { totalTime: loadTime, fileSize, url: imageUrl } } } catch (error) { return { type: 'preview-image', status: 'error', message: `预览图片加载失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 预览视频加载测试 const testPreviewVideoLoad = async (): Promise => { try { // 获取一个真实的视频素材 const response = await fetch('/api/materials?pageSize=1&materialType=主图视频') if (!response.ok) throw new Error('获取视频素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'preview-video', status: 'warning', message: '没有可测试的视频素材' } } const videoUrl = materials[0].videoPreviewClipUrl || materials[0].videoPreview540Url || materials[0].videoSourceUrl if (!videoUrl) { return { type: 'preview-video', status: 'warning', message: '视频素材没有预览URL' } } const startTime = performance.now() // 测试视频元数据加载(首字节时间) const video = document.createElement('video') video.preload = 'metadata' await new Promise((resolve, reject) => { video.onloadedmetadata = resolve video.onerror = reject video.src = videoUrl }) const metadataTime = performance.now() - startTime // 获取文件大小 const sizeResponse = await fetch(videoUrl, { method: 'HEAD' }) const fileSize = parseInt(sizeResponse.headers.get('content-length') || '0') return { type: 'preview-video', status: metadataTime > 5000 ? 'warning' : 'success', message: `元数据加载: ${(metadataTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(文件: ${(fileSize / 1024 / 1024).toFixed(2)}MB)` : ''}`, details: { ttfb: metadataTime, totalTime: metadataTime, fileSize, url: videoUrl } } } catch (error) { return { type: 'preview-video', status: 'error', message: `视频加载失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 音频文件加载测试 const testPreviewAudioLoad = async (): Promise => { try { // 获取一个真实的音频素材 const response = await fetch('/api/materials?pageSize=1&materialType=音乐') if (!response.ok) throw new Error('获取音频素材失败') const data = await response.json() const materials = data.materials || data if (!materials || materials.length === 0) { return { type: 'preview-audio', status: 'warning', message: '没有可测试的音频素材' } } const audioUrl = materials[0].zipFileName if (!audioUrl) { return { type: 'preview-audio', status: 'warning', message: '音频素材没有URL' } } const startTime = performance.now() // 测试音频元数据加载 const audio = new Audio() audio.preload = 'metadata' await new Promise((resolve, reject) => { audio.onloadedmetadata = resolve audio.onerror = reject audio.src = audioUrl }) const metadataTime = performance.now() - startTime // 获取文件大小 const sizeResponse = await fetch(audioUrl, { method: 'HEAD' }) const fileSize = parseInt(sizeResponse.headers.get('content-length') || '0') return { type: 'preview-audio', status: metadataTime > 3000 ? 'warning' : 'success', message: `元数据加载: ${(metadataTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(文件: ${(fileSize / 1024 / 1024).toFixed(2)}MB)` : ''}`, details: { ttfb: metadataTime, totalTime: metadataTime, fileSize, url: audioUrl } } } catch (error) { return { type: 'preview-audio', status: 'error', message: `音频加载失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // DNS和连接测试 const testDNSAndConnection = async (): Promise => { try { const testUrl = `${window.location.origin}/api/network-status` // 使用Resource Timing API获取详细的网络时序 const startMark = `dns-test-${Date.now()}` performance.mark(startMark) const startTime = performance.now() const response = await fetch(testUrl, { cache: 'no-store' }) const endTime = performance.now() if (!response.ok) throw new Error('连接测试失败') // 尝试获取Resource Timing信息 const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[] const entry = entries.find(e => e.name.includes('/api/network-status')) let dnsTime = 0 let connectTime = 0 let ttfb = 0 if (entry) { dnsTime = entry.domainLookupEnd - entry.domainLookupStart connectTime = entry.connectEnd - entry.connectStart ttfb = entry.responseStart - entry.requestStart } const totalTime = endTime - startTime return { type: 'connection', status: totalTime > 1000 ? 'warning' : 'success', message: `总耗时: ${totalTime.toFixed(0)}ms`, details: { dnsTime: dnsTime > 0 ? dnsTime : undefined, connectTime: connectTime > 0 ? connectTime : undefined, ttfb: ttfb > 0 ? ttfb : undefined, totalTime } } } catch (error) { return { type: 'connection', status: 'error', message: `连接测试失败: ${error instanceof Error ? error.message : '未知错误'}` } } } // 开始诊断 const startDiagnose = async () => { setTesting(true) setResults([]) // 1. DNS和连接测试 setResults([{ type: 'connection', status: 'testing' }]) const connectionResult = await testDNSAndConnection() setResults([connectionResult]) // 2. 下载测速 setResults(prev => [...prev, { type: 'download', status: 'testing' }]) const downloadResult = await testDownloadSpeed() setResults(prev => [...prev.slice(0, -1), downloadResult]) // 3. 上传测速 setResults(prev => [...prev, { type: 'upload', status: 'testing' }]) const uploadResult = await testUploadSpeed() setResults(prev => [...prev.slice(0, -1), uploadResult]) // 4. 存储连通性 setResults(prev => [...prev, { type: 'storage', status: 'testing' }]) const storageResult = await testStorageConnectivity() setResults(prev => [...prev.slice(0, -1), storageResult]) // 5. 预览图片加载测试 setResults(prev => [...prev, { type: 'preview-image', status: 'testing' }]) const imageResult = await testPreviewImageLoad() setResults(prev => [...prev.slice(0, -1), imageResult]) // 6. 预览视频加载测试 setResults(prev => [...prev, { type: 'preview-video', status: 'testing' }]) const videoResult = await testPreviewVideoLoad() setResults(prev => [...prev.slice(0, -1), videoResult]) // 7. 音频文件加载测试 setResults(prev => [...prev, { type: 'preview-audio', status: 'testing' }]) const audioResult = await testPreviewAudioLoad() setResults(prev => [...prev.slice(0, -1), audioResult]) setTesting(false) } // 复制结果(包含完整的诊断信息,用于AI分析) const copyResults = async () => { if (results.length === 0) return // 生成详细的诊断报告 const timestamp = new Date().toLocaleString('zh-CN') const report = [ '=== 网络诊断报告 ===', `时间: ${timestamp}`, `浏览器: ${navigator.userAgent}`, `当前URL: ${window.location.href}`, '', '=== 测试结果 ===', ...results.map(r => { const typeMap: Record = { 'download': '下载测速', 'upload': '上传测速', 'storage': '存储连通性', 'preview-image': '预览图片加载', 'preview-video': '预览视频加载', 'preview-audio': '音频文件加载', 'connection': 'DNS和连接测试' } const typeText = typeMap[r.type] || r.type const statusText = r.status === 'success' ? '✓ 成功' : r.status === 'error' ? '✗ 失败' : r.status === 'warning' ? '⚠ 警告' : '⋯ 测试中' let detailsText = '' if (r.details) { const details = [] if (r.details.dnsTime) details.push(`DNS解析: ${r.details.dnsTime.toFixed(0)}ms`) if (r.details.connectTime) details.push(`TCP连接: ${r.details.connectTime.toFixed(0)}ms`) if (r.details.ttfb) details.push(`首字节时间: ${r.details.ttfb.toFixed(0)}ms`) if (r.details.totalTime) details.push(`总耗时: ${r.details.totalTime.toFixed(0)}ms`) if (r.details.fileSize) details.push(`文件大小: ${(r.details.fileSize / 1024 / 1024).toFixed(2)}MB`) if (r.details.url) details.push(`URL: ${r.details.url}`) if (details.length > 0) { detailsText = '\n 详细信息: ' + details.join(', ') } } return `${typeText}: ${statusText}\n ${r.message || '测试中...'}${detailsText}` }), '', '=== 网络环境信息 ===', `在线状态: ${navigator.onLine ? '在线' : '离线'}`, `连接类型: ${(navigator as any).connection?.effectiveType || '未知'}`, `下行速度: ${(navigator as any).connection?.downlink || '未知'} Mbps`, `RTT延迟: ${(navigator as any).connection?.rtt || '未知'} ms`, '', '=== 性能分析 ===', ...results.filter(r => r.status === 'warning' || r.status === 'error').map(r => { const typeMap: Record = { 'preview-image': '预览图片', 'preview-video': '预览视频', 'preview-audio': '音频文件', 'connection': '网络连接' } const typeText = typeMap[r.type] || r.type return `⚠ ${typeText}加载较慢或失败,可能影响用户体验` }), '', '提示: 请将此报告提供给技术支持以便快速定位问题' ].join('\n') try { await navigator.clipboard.writeText(report) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch (error) { console.error('复制失败,尝试降级方案:', error) // 降级方案:使用传统方法复制 const textarea = document.createElement('textarea') textarea.value = report textarea.style.position = 'fixed' textarea.style.left = '-999999px' textarea.style.top = '-999999px' document.body.appendChild(textarea) textarea.focus() textarea.select() try { const successful = document.execCommand('copy') if (successful) { setCopied(true) setTimeout(() => setCopied(false), 2000) } } catch (err) { console.error('降级复制也失败:', err) showToast('复制失败,请手动复制结果', { variant: 'error' }) } document.body.removeChild(textarea) } } const getStatusIcon = (status: string) => { switch (status) { case 'testing': return ( ) case 'success': return ( ) case 'warning': return ( ) case 'error': return ( ) default: return null } } const getTypeText = (type: string) => { switch (type) { case 'download': return '下载测速' case 'upload': return '上传测速' case 'storage': return '存储连通性' case 'preview-image': return '预览图片加载' case 'preview-video': return '预览视频加载' case 'preview-audio': return '音频文件加载' case 'connection': return 'DNS和连接' default: return '' } } if (!isOpen) return null return (
{/* Header */}

网络诊断

测试网络连接速度和存储服务状态

{/* Content */}
{results.length === 0 ? (

点击下方按钮开始诊断

将测试网络速度、预览文件加载等性能指标

) : (
{results.map((result, index) => (
{getStatusIcon(result.status)}

{getTypeText(result.type)}

{result.message || '测试中...'}

{result.details && (
{result.details.dnsTime !== undefined && (
DNS解析: {result.details.dnsTime.toFixed(0)}ms
)} {result.details.connectTime !== undefined && (
TCP连接: {result.details.connectTime.toFixed(0)}ms
)} {result.details.ttfb !== undefined && (
首字节时间: {result.details.ttfb.toFixed(0)}ms
)}
)}
))}
)}
{/* Footer */}
{results.length > 0 && !testing && ( )}
{/* 统一的Toast提示 */} {toast && toast.show && ( )}
) }