From e25a23bd30257f8a4ce2984f4aa5e7b0f5daf560 Mon Sep 17 00:00:00 2001 From: MatrixSeven Date: Fri, 1 Aug 2025 20:13:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B3=A8=E9=87=8A=E6=8E=89=E9=9D=99?= =?UTF-8?q?=E6=80=81=E6=96=87=E4=BB=B6=E6=9C=8D=E5=8A=A1=E5=92=8C=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/main.go | 6 +- internal/handlers/handlers.go | 2 +- web/static/css/style.css | 1010 -------------------- web/static/js/common.js | 393 -------- web/static/js/file-transfer.js | 373 -------- web/static/js/p2p-transfer-new.js | 1433 ---------------------------- web/static/js/webrtc-connection.js | 902 ----------------- web/static/test-websocket.html | 50 - web/static/websocket-debug.html | 127 --- web/templates/base.html | 80 -- web/templates/index.html | 353 ------- web/templates/index_clean.html | 0 web/templates/upload.html | 353 ------- web/templates/video.html | 571 ----------- 14 files changed, 4 insertions(+), 5649 deletions(-) delete mode 100644 web/static/css/style.css delete mode 100644 web/static/js/common.js delete mode 100644 web/static/js/file-transfer.js delete mode 100644 web/static/js/p2p-transfer-new.js delete mode 100644 web/static/js/webrtc-connection.js delete mode 100644 web/static/test-websocket.html delete mode 100644 web/static/websocket-debug.html delete mode 100644 web/templates/base.html delete mode 100644 web/templates/index.html delete mode 100644 web/templates/index_clean.html delete mode 100644 web/templates/upload.html delete mode 100644 web/templates/video.html diff --git a/cmd/main.go b/cmd/main.go index 79f1838..c2898e9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -42,9 +42,9 @@ func main() { MaxAge: 300, })) - // 静态文件服务 - fileServer := http.FileServer(http.Dir("./web/static/")) - r.Handle("/static/*", http.StripPrefix("/static", fileServer)) + // // 静态文件服务 + // fileServer := http.FileServer(http.Dir("./web/static/")) + // r.Handle("/static/*", http.StripPrefix("/static", fileServer)) // 路由定义 r.Get("/", h.IndexHandler) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 235a3c8..3d2fddd 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -22,7 +22,7 @@ func NewHandler(p2pService *services.P2PService) *Handler { } // 加载模板 - h.loadTemplates() + // h.loadTemplates() return h } diff --git a/web/static/css/style.css b/web/static/css/style.css deleted file mode 100644 index 4546fa4..0000000 --- a/web/static/css/style.css +++ /dev/null @@ -1,1010 +0,0 @@ -/* 自定义样式 - 补充Tailwind CSS */ - -/* 全局样式 */ -* { - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; - line-height: 1.6; - background: #f9fafb; -} - -/* 自定义scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: #f1f5f9; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb { - background: #cbd5e1; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: #94a3b8; -} - -/* 文件拖拽区域样式 - 增强视觉效果 */ -#fileDropZone { - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - cursor: pointer; - position: relative; - overflow: hidden; -} - -#fileDropZone::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.1), transparent); - transition: left 0.6s; -} - -#fileDropZone:hover::before { - left: 100%; -} - -#fileDropZone.drag-over { - border-color: #3b82f6 !important; - background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%) !important; - transform: scale(1.02); - box-shadow: 0 10px 25px rgba(59, 130, 246, 0.15); -} - -/* 按钮增强效果 */ -button { - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; -} - -button:hover:not(:disabled) { - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -button:active:not(:disabled) { - transform: translateY(0); -} - -button:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -/* 按钮加载效果 */ -button.loading::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 16px; - height: 16px; - margin: -8px 0 0 -8px; - border: 2px solid transparent; - border-top-color: currentColor; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -/* 卡片悬停效果 */ -.bg-white { - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.bg-white:hover { - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); - transform: translateY(-2px); -} - -/* 文件项样式 */ -.file-item { - transition: all 0.2s ease; - border: 1px solid transparent; -} - -.file-item:hover { - background: #f8fafc; - border-color: #e2e8f0; - transform: translateX(4px); -} - -/* 进度条样式 */ -.progress-bar { - background: linear-gradient(90deg, #10b981, #059669); - border-radius: 8px; - position: relative; - overflow: hidden; -} - -.progress-bar::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); - animation: shimmer 2s infinite; -} - -@keyframes shimmer { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(100%); - } -} - -/* 状态指示器增强 */ -.status-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - display: inline-block; - margin-right: 8px; - position: relative; -} - -.status-connected { - background: #10b981; - box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2); -} - -.status-connecting { - background: #f59e0b; - animation: pulse-status 2s infinite; -} - -.status-disconnected { - background: #ef4444; -} - -@keyframes pulse-status { - 0%, 100% { - opacity: 1; - transform: scale(1); - } - 50% { - opacity: 0.7; - transform: scale(1.2); - } -} - -/* 输入框增强 */ -input[type="text"], input[type="file"] { - transition: all 0.2s ease; -} - -input[type="text"]:focus { - transform: translateY(-1px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); -} - -/* 取件码显示增强 */ -#pickupCodeDisplay { - position: relative; - background: linear-gradient(135deg, #10b981, #059669); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -/* 微交互和动画效果 */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes fadeInDown { - from { - opacity: 0; - transform: translateY(-30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes slideInRight { - from { - opacity: 0; - transform: translateX(30px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes slideInLeft { - from { - opacity: 0; - transform: translateX(-30px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes scaleIn { - from { - opacity: 0; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes pulse-slow { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.7; - } -} - -.fade-in-up { - animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1); -} - -.fade-in-down { - animation: fadeInDown 0.6s cubic-bezier(0.4, 0, 0.2, 1); -} - -.slide-in-right { - animation: slideInRight 0.6s cubic-bezier(0.4, 0, 0.2, 1); -} - -.slide-in-left { - animation: slideInLeft 0.6s cubic-bezier(0.4, 0, 0.2, 1); -} - -.scale-in { - animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* 页面加载动画 */ -.animate-on-scroll { - opacity: 0; - transform: translateY(20px); - transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); -} - -.animate-on-scroll.visible { - opacity: 1; - transform: translateY(0); -} - -/* 悬停时的微妙变化 */ -.hover-lift { - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.hover-lift:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); -} - -/* 点击反馈 */ -.click-feedback { - position: relative; - overflow: hidden; -} - -.click-feedback::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - border-radius: 50%; - background: rgba(255, 255, 255, 0.5); - transform: translate(-50%, -50%); - transition: width 0.6s, height 0.6s; -} - -.click-feedback:active::after { - width: 300px; - height: 300px; -} - -/* 渐变文字效果 */ -.gradient-text { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -/* 成功/错误提示动画增强 */ -@keyframes bounce { - 0%, 20%, 53%, 80%, 100% { - transform: translate3d(0,0,0); - } - 40%, 43% { - transform: translate3d(0, -20px, 0); - } - 70% { - transform: translate3d(0, -10px, 0); - } - 90% { - transform: translate3d(0, -4px, 0); - } -} - -.success-bounce { - animation: bounce 1s ease; -} - -@keyframes shake { - 0%, 100% { - transform: translateX(0); - } - 10%, 30%, 50%, 70%, 90% { - transform: translateX(-8px); - } - 20%, 40%, 60%, 80% { - transform: translateX(8px); - } -} - -.error-shake { - animation: shake 0.5s ease-in-out; -} - -/* 加载状态增强 */ -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -@keyframes loading-dots { - 0%, 20% { - color: rgba(0, 0, 0, 0.4); - text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0.2), - 0.5em 0 0 rgba(0, 0, 0, 0.2); - } - 40% { - color: rgba(0, 0, 0, 1); - text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0.4), - 0.5em 0 0 rgba(0, 0, 0, 0.2); - } - 60% { - text-shadow: 0.25em 0 0 rgba(0, 0, 0, 1), - 0.5em 0 0 rgba(0, 0, 0, 0.4); - } - 80%, 100% { - text-shadow: 0.25em 0 0 rgba(0, 0, 0, 1), - 0.5em 0 0 rgba(0, 0, 0, 1); - } -} - -.loading-dots::after { - content: '...'; - animation: loading-dots 1.4s infinite; -} - -/* 卡片阴影层次 */ -.shadow-soft { - box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04); -} - -.shadow-medium { - box-shadow: 0 4px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); -} - -.shadow-strong { - box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.25); -} - -/* 焦点状态增强 */ -.focus-ring { - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -.focus-ring:focus { - outline: none; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); - border-color: #3b82f6; -} - -/* 连接状态指示器 */ -.connection-pulse { - position: relative; -} - -.connection-pulse::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 100%; - height: 100%; - border-radius: 50%; - background: currentColor; - opacity: 0.3; - transform: translate(-50%, -50%); - animation: pulse-ring 2s infinite; -} - -@keyframes pulse-ring { - 0% { - transform: translate(-50%, -50%) scale(0.8); - opacity: 0.3; - } - 50% { - transform: translate(-50%, -50%) scale(1.2); - opacity: 0.1; - } - 100% { - transform: translate(-50%, -50%) scale(1.4); - opacity: 0; - } -} - -/* 响应式设计优化 */ -@media (max-width: 640px) { - .container { - padding-left: 0.75rem; - padding-right: 0.75rem; - } - - .lg\\:grid-cols-2 { - grid-template-columns: 1fr; - gap: 1.5rem; - } - - .text-3xl { - font-size: 1.75rem; - line-height: 2rem; - } - - .text-2xl { - font-size: 1.25rem; - line-height: 1.75rem; - } - - .text-xl { - font-size: 1.125rem; - line-height: 1.75rem; - } - - .px-6 { - padding-left: 1rem; - padding-right: 1rem; - } - - .py-6 { - padding-top: 1rem; - padding-bottom: 1rem; - } - - .px-4 { - padding-left: 0.75rem; - padding-right: 0.75rem; - } - - .py-4 { - padding-top: 0.75rem; - padding-bottom: 0.75rem; - } - - #fileDropZone { - padding: 1.5rem 1rem; - } - - .rounded-2xl { - border-radius: 1rem; - } - - .rounded-xl { - border-radius: 0.75rem; - } - - /* 移动端按钮优化 */ - button { - min-height: 44px; - touch-action: manipulation; - } - - /* 移动端输入框优化 */ - input[type="text"] { - font-size: 16px; /* 防止iOS Safari缩放 */ - min-height: 44px; - } - - /* 移动端文件项优化 */ - .file-item { - padding: 1rem; - } - - .file-item .w-10 { - width: 2.5rem; - height: 2.5rem; - } - - .file-item .w-12 { - width: 2.5rem; - height: 2.5rem; - } - - /* 移动端进度条优化 */ - #transferProgress .px-6 { - padding-left: 1rem; - padding-right: 1rem; - } - - /* 移动端通知优化 */ - .notification { - left: 1rem; - right: 1rem; - top: 1rem; - width: auto; - transform: translateY(-100%); - } - - .notification.show { - transform: translateY(0); - } -} - -@media (max-width: 768px) { - .max-w-6xl { - max-width: 100%; - } - - .grid.lg\\:grid-cols-2 { - grid-template-columns: 1fr; - gap: 1.5rem; - } - - #transferProgress { - margin-top: 1.5rem; - } - - /* 平板端优化 */ - .text-3xl { - font-size: 2rem; - line-height: 2.25rem; - } -} - -/* 大屏幕优化 */ -@media (min-width: 1280px) { - .max-w-6xl { - max-width: 80rem; - } - - .container { - padding-left: 2rem; - padding-right: 2rem; - } -} - -/* 深色模式支持 */ -@media (prefers-color-scheme: dark) { - body { - background: #111827; - color: #f9fafb; - } - - .bg-gray-50 { - background: #111827; - } - - .bg-white { - background: #1f2937; - color: #f9fafb; - } - - .text-gray-600 { - color: #d1d5db; - } - - .text-gray-700 { - color: #f3f4f6; - } - - .text-gray-900 { - color: #f9fafb; - } - - .border-gray-100 { - border-color: #374151; - } - - .border-gray-200 { - border-color: #4b5563; - } - - .border-gray-300 { - border-color: #6b7280; - } - - input[type="text"] { - background: #374151; - color: #f9fafb; - border-color: #4b5563; - } - - input[type="text"]:focus { - border-color: #10b981; - background: #1f2937; - } - - #fileDropZone { - background: #1f2937; - border-color: #4b5563; - } - - #fileDropZone:hover { - background: #374151; - border-color: #6b7280; - } -} - -/* 打印样式优化 */ -@media print { - body { - background: white !important; - color: black !important; - } - - .bg-white, .bg-gray-50 { - background: white !important; - box-shadow: none !important; - } - - button, .no-print { - display: none !important; - } - - .rounded-2xl { - border-radius: 0; - border: 1px solid #ccc; - } -} - -/* 高对比度支持 */ -@media (prefers-contrast: high) { - button { - border: 2px solid currentColor; - font-weight: bold; - } - - .border { - border-width: 2px; - } - - input[type="text"] { - border-width: 2px; - } - - #fileDropZone { - border-width: 3px; - } -} - -/* 减少动画支持 */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - - .fade-in-up, .fade-in-down, .slide-in-right, .slide-in-left { - animation: none !important; - } -} - -/* 辅助功能增强 */ -@media (prefers-reduced-motion: no-preference) { - html { - scroll-behavior: smooth; - } -} - -/* Focus可见性增强 */ -button:focus-visible, -input:focus-visible, -details:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 2px; -} - -/* 高分辨率屏幕优化 */ -@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { - .status-indicator { - border-radius: 50%; - background-clip: padding-box; - } -} - -/* 触摸设备优化 */ -@media (hover: none) and (pointer: coarse) { - button { - min-height: 44px; - min-width: 44px; - } - - input[type="text"] { - font-size: 16px; /* 防止iOS缩放 */ - } - - .hover\\:bg-blue-600:hover { - background-color: #2563eb; - } -} - -/* 工具提示样式增强 */ -.tooltip { - position: relative; - display: inline-block; -} - -.tooltip::after { - content: attr(data-tooltip); - position: absolute; - bottom: 125%; - left: 50%; - transform: translateX(-50%); - background: #1f2937; - color: white; - padding: 0.5rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.75rem; - white-space: nowrap; - opacity: 0; - visibility: hidden; - transition: all 0.2s ease; - z-index: 1000; -} - -.tooltip::before { - content: ''; - position: absolute; - bottom: 115%; - left: 50%; - transform: translateX(-50%); - border: 5px solid transparent; - border-top-color: #1f2937; - opacity: 0; - visibility: hidden; - transition: all 0.2s ease; -} - -.tooltip:hover::after, -.tooltip:hover::before { - opacity: 1; - visibility: visible; -} - -/* 加载状态样式 */ -.skeleton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; -} - -@keyframes loading { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } -} - -/* 通知样式 */ -.notification { - position: fixed; - top: 20px; - right: 20px; - background: white; - border-radius: 0.75rem; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); - border: 1px solid #e5e7eb; - padding: 1rem 1.5rem; - z-index: 1000; - transform: translateX(100%); - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.notification.show { - transform: translateX(0); -} - -.notification.success { - border-left: 4px solid #10b981; -} - -.notification.error { - border-left: 4px solid #ef4444; -} - -.notification.warning { - border-left: 4px solid #f59e0b; -} - -.notification.info { - border-left: 4px solid #3b82f6; -} - -/* 自定义选择样式 */ -::selection { - background: #3b82f6; - color: white; -} - -::-moz-selection { - background: #3b82f6; - color: white; -} - -/* 代码块样式优化 */ -pre, code { - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Source Code Pro', monospace; - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - padding: 0.25rem 0.5rem; - font-size: 0.875rem; -} - -pre { - padding: 1rem; - overflow-x: auto; - white-space: pre-wrap; - word-wrap: break-word; - line-height: 1.5; -} - -/* 标签页样式 */ -.tab-button { - position: relative; - transition: all 0.3s ease; -} - -.tab-button::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - right: 0; - height: 2px; - background: currentColor; - transform: scaleX(0); - transition: transform 0.3s ease; -} - -.tab-button.active::after { - transform: scaleX(1); -} - -.tab-button:hover { - transform: translateY(-1px); -} - -.tab-content { - opacity: 0; - transform: translateY(10px); - transition: all 0.3s ease; -} - -.tab-content.active { - opacity: 1; - transform: translateY(0); -} - -.tab-content.hidden { - display: none !important; -} - -/* 标签页切换动画 */ -@keyframes tab-slide-in { - from { - opacity: 0; - transform: translateX(20px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.tab-content.active { - animation: tab-slide-in 0.3s ease; -} - -/* 取件码点击效果 */ -#pickupCodeDisplay, #pickupLinkDisplay { - cursor: pointer; - transition: all 0.2s ease; -} - -#pickupCodeDisplay:hover, #pickupLinkDisplay:hover { - background: rgba(34, 197, 94, 0.1); - transform: scale(1.02); -} - -/* 成功反馈动画 */ -.success-bounce { - animation: success-bounce 0.6s ease; - color: #16a34a !important; -} - -@keyframes success-bounce { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.05); } -} - -/* 紧凑布局优化 */ -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} - -/* 选择文本样式 */ -.select-all { - user-select: all; -} - -/* 固定Header样式 */ -.fixed { - position: fixed; -} - -.backdrop-blur-sm { - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); -} - -.bg-white\/95 { - background-color: rgba(255, 255, 255, 0.95); -} - -.z-50 { - z-index: 50; -} - -/* 主内容区域顶部边距 */ -.pt-20 { - padding-top: 5rem; -} - -/* Header动画 */ -.fixed-header { - transition: all 0.3s ease; -} - -/* 页面滚动时Header阴影 */ -.header-shadow { - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} diff --git a/web/static/js/common.js b/web/static/js/common.js deleted file mode 100644 index 024ef1e..0000000 --- a/web/static/js/common.js +++ /dev/null @@ -1,393 +0,0 @@ -// 通用JavaScript工具函数 - -// 工具函数 -const Utils = { - // 格式化文件大小 - formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - }, - - // 格式化时间 - formatTime(date) { - return new Date(date).toLocaleString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - }, - - // 生成随机字符串 - randomString(length) { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; - }, - - // 复制到剪贴板 - async copyToClipboard(text) { - try { - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(text); - return true; - } else { - // 兼容性处理 - const textArea = document.createElement('textarea'); - textArea.value = text; - textArea.style.position = 'fixed'; - textArea.style.left = '-999999px'; - textArea.style.top = '-999999px'; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - const result = document.execCommand('copy'); - document.body.removeChild(textArea); - return result; - } - } catch (error) { - console.error('复制失败:', error); - return false; - } - }, - - // 获取文件类型图标 - getFileIcon(fileName, fileType) { - const ext = fileName.split('.').pop().toLowerCase(); - - // 根据MIME类型 - if (fileType) { - if (fileType.startsWith('image/')) return '🖼️'; - if (fileType.startsWith('video/')) return '🎥'; - if (fileType.startsWith('audio/')) return '🎵'; - if (fileType.includes('pdf')) return '📄'; - if (fileType.includes('text')) return '📝'; - if (fileType.includes('zip') || fileType.includes('rar')) return '📦'; - } - - // 根据文件扩展名 - switch (ext) { - case 'pdf': return '📄'; - case 'doc': - case 'docx': return '📝'; - case 'xls': - case 'xlsx': return '📊'; - case 'ppt': - case 'pptx': return '📈'; - case 'txt': return '📄'; - case 'epub': - case 'mobi': return '📚'; - case 'zip': - case 'rar': - case '7z': return '📦'; - case 'jpg': - case 'jpeg': - case 'png': - case 'gif': - case 'bmp': return '🖼️'; - case 'mp4': - case 'avi': - case 'mov': - case 'wmv': return '🎥'; - case 'mp3': - case 'wav': - case 'flac': - case 'aac': return '🎵'; - case 'js': - case 'html': - case 'css': - case 'py': - case 'java': - case 'cpp': return '💻'; - default: return '📁'; - } - }, - - // 验证取件码格式 - validateCode(code) { - return /^[A-Z0-9]{6}$/.test(code); - }, - - // 获取浏览器信息 - getBrowserInfo() { - const ua = navigator.userAgent; - let browser = 'Unknown'; - let version = 'Unknown'; - - if (ua.indexOf('Chrome') > -1) { - browser = 'Chrome'; - version = ua.match(/Chrome\/(\d+)/)[1]; - } else if (ua.indexOf('Firefox') > -1) { - browser = 'Firefox'; - version = ua.match(/Firefox\/(\d+)/)[1]; - } else if (ua.indexOf('Safari') > -1 && ua.indexOf('Chrome') === -1) { - browser = 'Safari'; - version = ua.match(/Version\/(\d+)/)[1]; - } else if (ua.indexOf('Edge') > -1) { - browser = 'Edge'; - version = ua.match(/Edge\/(\d+)/)[1]; - } else if (ua.indexOf('360SE') > -1) { - browser = '360浏览器'; - } else if (ua.indexOf('QQBrowser') > -1) { - browser = 'QQ浏览器'; - version = ua.match(/QQBrowser\/(\d+)/)[1]; - } - - return { browser, version }; - }, - - // 检查WebRTC支持 - checkWebRTCSupport() { - return !!(window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection); - }, - - // 检查文件API支持 - checkFileAPISupport() { - return !!(window.File && window.FileReader && window.FileList && window.Blob); - }, - - // 节流函数 - throttle(func, limit) { - let inThrottle; - return function() { - const args = arguments; - const context = this; - if (!inThrottle) { - func.apply(context, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - } - }, - - // 防抖函数 - debounce(func, delay) { - let timeoutId; - return function() { - const args = arguments; - const context = this; - clearTimeout(timeoutId); - timeoutId = setTimeout(() => func.apply(context, args), delay); - } - } -}; - -// 通知系统 -const Notification = { - // 显示成功消息 - success(message, duration = 3000) { - this.show(message, 'success', duration); - }, - - // 显示错误消息 - error(message, duration = 5000) { - this.show(message, 'error', duration); - }, - - // 显示警告消息 - warning(message, duration = 4000) { - this.show(message, 'warning', duration); - }, - - // 显示信息消息 - info(message, duration = 3000) { - this.show(message, 'info', duration); - }, - - // 显示通知 - show(message, type = 'info', duration = 3000) { - // 创建通知容器(如果不存在) - let container = document.getElementById('notification-container'); - if (!container) { - container = document.createElement('div'); - container.id = 'notification-container'; - container.className = 'fixed top-4 right-4 z-50 space-y-2'; - document.body.appendChild(container); - } - - // 创建通知元素 - const notification = document.createElement('div'); - notification.className = `max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5 fade-in`; - - const bgColor = { - success: 'bg-green-50 border-green-200', - error: 'bg-red-50 border-red-200', - warning: 'bg-yellow-50 border-yellow-200', - info: 'bg-blue-50 border-blue-200' - }[type] || 'bg-gray-50 border-gray-200'; - - const iconEmoji = { - success: '✅', - error: '❌', - warning: '⚠️', - info: 'ℹ️' - }[type] || 'ℹ️'; - - notification.innerHTML = ` -
-
-
- ${iconEmoji} -
-
-

${message}

-
-
-
-
- -
- `; - - notification.className += ` ${bgColor}`; - container.appendChild(notification); - - // 自动移除 - if (duration > 0) { - setTimeout(() => { - if (notification.parentNode) { - notification.style.opacity = '0'; - notification.style.transform = 'translateX(100%)'; - setTimeout(() => notification.remove(), 300); - } - }, duration); - } - } -}; - -// 加载管理器 -const Loading = { - show(message = '加载中...') { - this.hide(); // 先隐藏现有的加载提示 - - const overlay = document.createElement('div'); - overlay.id = 'loading-overlay'; - overlay.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; - - overlay.innerHTML = ` -
-
- ${message} -
- `; - - document.body.appendChild(overlay); - }, - - hide() { - const overlay = document.getElementById('loading-overlay'); - if (overlay) { - overlay.remove(); - } - } -}; - -// API请求工具 -const API = { - async request(url, options = {}) { - const defaultOptions = { - headers: { - 'Content-Type': 'application/json', - }, - }; - - const config = { ...defaultOptions, ...options }; - - try { - const response = await fetch(url, config); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - return await response.json(); - } else { - return await response.text(); - } - } catch (error) { - console.error('API请求失败:', error); - throw error; - } - }, - - async get(url, params = {}) { - const urlObj = new URL(url, window.location.origin); - Object.keys(params).forEach(key => - urlObj.searchParams.append(key, params[key]) - ); - - return this.request(urlObj.toString()); - }, - - async post(url, data = {}) { - return this.request(url, { - method: 'POST', - body: JSON.stringify(data), - }); - }, - - async delete(url) { - return this.request(url, { - method: 'DELETE', - }); - } -}; - -// 页面加载完成后执行 -document.addEventListener('DOMContentLoaded', function() { - // 检查浏览器兼容性 - const browserInfo = Utils.getBrowserInfo(); - console.log(`浏览器: ${browserInfo.browser} ${browserInfo.version}`); - - // 检查功能支持 - if (!Utils.checkFileAPISupport()) { - Notification.warning('您的浏览器不完全支持文件API,部分功能可能受限'); - } - - if (!Utils.checkWebRTCSupport()) { - console.warn('浏览器不支持WebRTC,视频功能不可用'); - } - - // 添加全局错误处理 - window.addEventListener('error', function(event) { - console.error('全局错误:', event.error); - Notification.error('页面发生错误,请刷新后重试'); - }); - - // 添加网络状态监听 - window.addEventListener('online', function() { - Notification.success('网络连接已恢复'); - }); - - window.addEventListener('offline', function() { - Notification.warning('网络连接已断开,请检查网络设置'); - }); - - // 添加页面可见性变化监听 - document.addEventListener('visibilitychange', function() { - if (document.hidden) { - console.log('页面已隐藏'); - } else { - console.log('页面已显示'); - } - }); -}); - -// 导出全局对象 -window.Utils = Utils; -window.Notification = Notification; -window.Loading = Loading; -window.API = API; diff --git a/web/static/js/file-transfer.js b/web/static/js/file-transfer.js deleted file mode 100644 index 338f90b..0000000 --- a/web/static/js/file-transfer.js +++ /dev/null @@ -1,373 +0,0 @@ -// 文件传输相关功能 - -// 设置数据通道 -function setupDataChannel(channel) { - dataChannel = channel; - let pendingChunkMeta = null; - - channel.onopen = () => { - console.log('数据通道已打开'); - isP2PConnected = true; - updateP2PStatus(true); - - // 清除连接超时定时器 - if (connectionTimeout) { - clearTimeout(connectionTimeout); - connectionTimeout = null; - } - }; - - channel.onmessage = (event) => { - // 检查是否是二进制数据 - if (event.data instanceof ArrayBuffer) { - // 处理二进制数据块 - if (pendingChunkMeta && currentRole === 'receiver') { - receiveFileChunk(pendingChunkMeta, event.data); - pendingChunkMeta = null; - } - } else { - // 处理JSON消息 - try { - const message = JSON.parse(event.data); - if (message.type === 'file-chunk-meta') { - pendingChunkMeta = message; - } else { - handleDataChannelMessage(event.data); - } - } catch (error) { - console.error('解析数据通道消息失败:', error); - } - } - }; - - channel.onerror = (error) => { - console.error('数据通道错误:', error); - isP2PConnected = false; - updateP2PStatus(false); - }; - - channel.onclose = () => { - console.log('数据通道已关闭'); - isP2PConnected = false; - updateP2PStatus(false); - }; -} - -// 更新P2P连接状态 -function updateP2PStatus(connected) { - const receiverStatus = document.getElementById('receiverStatus'); - const downloadButtons = document.querySelectorAll('button[onclick^="downloadFile"]'); - - if (currentRole === 'receiver' && receiverStatus) { - if (connected) { - receiverStatus.innerHTML = ` -
- - P2P连接已建立,可以下载文件 -
`; - - // 启用下载按钮 - downloadButtons.forEach(btn => { - btn.disabled = false; - btn.classList.remove('opacity-50', 'cursor-not-allowed'); - btn.classList.add('hover:bg-blue-600'); - }); - } else { - receiverStatus.innerHTML = ` -
- - 正在建立P2P连接... -
`; - - // 禁用下载按钮 - downloadButtons.forEach(btn => { - btn.disabled = true; - btn.classList.add('opacity-50', 'cursor-not-allowed'); - btn.classList.remove('hover:bg-blue-600'); - }); - } - } -} - -// 下载文件 -function downloadFile(fileId) { - if (!isP2PConnected || !dataChannel || dataChannel.readyState !== 'open') { - alert('P2P连接未建立,请等待连接建立后重试'); - return; - } - - // 发送文件请求 - const request = { - type: 'file-request', - fileId: fileId - }; - - dataChannel.send(JSON.stringify(request)); - showTransferProgress(fileId, 'downloading'); -} - -// 处理数据通道消息 -function handleDataChannelMessage(data) { - try { - const message = JSON.parse(data); - - switch (message.type) { - case 'file-request': - if (currentRole === 'sender') { - sendFileData(message.fileId); - } - break; - - case 'file-info': - if (currentRole === 'receiver') { - // 存储文件信息用于下载 - if (!fileTransfers.has(message.fileId)) { - fileTransfers.set(message.fileId, { - chunks: [], - totalSize: message.size, - receivedSize: 0, - fileName: message.name, - mimeType: message.mimeType - }); - } - } - break; - - case 'file-data': - // 旧的file-data类型已被file-chunk-meta + 二进制数据替代 - // 这里保留是为了向后兼容 - if (currentRole === 'receiver') { - receiveFileDataLegacy(message); - } - break; - - case 'file-complete': - if (currentRole === 'receiver') { - completeFileDownload(message.fileId); - } - break; - } - } catch (error) { - console.error('处理数据通道消息失败:', error); - } -} - -// 发送文件数据 -function sendFileData(fileId) { - const fileIndex = parseInt(fileId.split('_')[1]); - const file = selectedFiles[fileIndex]; - - if (!file) return; - - // 首先发送文件元信息 - const fileInfo = { - type: 'file-info', - fileId: fileId, - name: file.name, - size: file.size, - mimeType: file.type, - lastModified: file.lastModified - }; - dataChannel.send(JSON.stringify(fileInfo)); - - const reader = new FileReader(); - const chunkSize = 65536; // 增加到64KB chunks以提高速度 - let offset = 0; - - const sendChunk = () => { - const slice = file.slice(offset, offset + chunkSize); - reader.readAsArrayBuffer(slice); - }; - - reader.onload = (e) => { - const chunk = e.target.result; - - // 使用更高效的方式传输二进制数据 - if (dataChannel.readyState === 'open') { - // 先发送元数据 - const metadata = { - type: 'file-chunk-meta', - fileId: fileId, - offset: offset, - size: chunk.byteLength, - total: file.size, - isLast: offset + chunk.byteLength >= file.size - }; - dataChannel.send(JSON.stringify(metadata)); - - // 再发送二进制数据 - dataChannel.send(chunk); - } - - offset += chunk.byteLength; - - if (offset < file.size) { - // 减少延迟以提高传输速度 - setTimeout(sendChunk, 1); - } else { - dataChannel.send(JSON.stringify({ - type: 'file-complete', - fileId: fileId - })); - } - }; - - sendChunk(); -} - -// 接收文件块(二进制数据) -function receiveFileChunk(meta, chunkData) { - if (!fileTransfers.has(meta.fileId)) { - // 如果没有文件信息,创建默认的 - fileTransfers.set(meta.fileId, { - chunks: [], - totalSize: meta.total, - receivedSize: 0, - fileName: `unknown_file_${meta.fileId}`, - mimeType: 'application/octet-stream' - }); - } - - const transfer = fileTransfers.get(meta.fileId); - transfer.chunks.push(new Uint8Array(chunkData)); - transfer.receivedSize += chunkData.byteLength; - - // 更新总大小(以防文件信息还没收到) - if (transfer.totalSize !== meta.total) { - transfer.totalSize = meta.total; - } - - // 更新进度 - updateTransferProgress(meta.fileId, transfer.receivedSize, transfer.totalSize); - - if (meta.isLast) { - completeFileDownload(meta.fileId); - } -} - -// 接收文件数据(向后兼容的旧版本) -function receiveFileDataLegacy(message) { - if (!fileTransfers.has(message.fileId)) { - // 如果没有文件信息,创建默认的 - fileTransfers.set(message.fileId, { - chunks: [], - totalSize: message.total, - receivedSize: 0, - fileName: `unknown_file_${message.fileId}`, - mimeType: 'application/octet-stream' - }); - } - - const transfer = fileTransfers.get(message.fileId); - transfer.chunks.push(new Uint8Array(message.chunk)); - transfer.receivedSize += message.chunk.length; - - // 更新总大小(以防文件信息还没收到) - if (transfer.totalSize !== message.total) { - transfer.totalSize = message.total; - } - - // 更新进度 - updateTransferProgress(message.fileId, transfer.receivedSize, transfer.totalSize); - - if (message.isLast) { - completeFileDownload(message.fileId); - } -} - -// 完成文件下载 -function completeFileDownload(fileId) { - const transfer = fileTransfers.get(fileId); - if (!transfer) return; - - // 合并所有chunks,使用正确的MIME类型 - const blob = new Blob(transfer.chunks, { type: transfer.mimeType }); - - // 使用正确的文件名 - const fileName = transfer.fileName || `downloaded_file_${fileId}`; - - // 创建下载链接 - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - a.style.display = 'none'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - console.log(`文件下载完成: ${fileName}, 大小: ${formatFileSize(transfer.totalSize)}`); - - // 清理 - fileTransfers.delete(fileId); - hideTransferProgress(fileId); -} - -// 显示传输进度 -function showTransferProgress(fileId, type) { - const progressContainer = document.getElementById('transferProgress'); - const progressList = document.getElementById('progressList'); - - progressContainer.classList.remove('hidden'); - - // 获取文件名 - let fileName = fileId; - if (currentRole === 'receiver') { - // 从接收方文件列表中获取文件名 - const fileIndex = parseInt(fileId.split('_')[1]); - const receiverFilesList = document.getElementById('receiverFilesList'); - const fileItems = receiverFilesList.querySelectorAll('.font-medium'); - if (fileItems[fileIndex]) { - fileName = fileItems[fileIndex].textContent; - } - } else if (currentRole === 'sender') { - // 从发送方文件列表中获取文件名 - const fileIndex = parseInt(fileId.split('_')[1]); - if (selectedFiles[fileIndex]) { - fileName = selectedFiles[fileIndex].name; - } - } - - const progressItem = document.createElement('div'); - progressItem.id = `progress_${fileId}`; - progressItem.className = 'bg-gray-50 p-3 rounded-lg'; - progressItem.innerHTML = ` -
- ${type === 'downloading' ? '📥 下载' : '📤 上传'}: ${fileName} - 0% -
-
-
-
- `; - - progressList.appendChild(progressItem); -} - -// 更新传输进度 -function updateTransferProgress(fileId, received, total) { - const progressItem = document.getElementById(`progress_${fileId}`); - if (!progressItem) return; - - const percentage = Math.round((received / total) * 100); - const progressBar = progressItem.querySelector('.bg-blue-600'); - const percentageText = progressItem.querySelector('.text-gray-500'); - - progressBar.style.width = percentage + '%'; - percentageText.textContent = percentage + '%'; -} - -// 隐藏传输进度 -function hideTransferProgress(fileId) { - const progressItem = document.getElementById(`progress_${fileId}`); - if (progressItem) { - progressItem.remove(); - } - - // 如果没有进度项了,隐藏整个进度容器 - const progressList = document.getElementById('progressList'); - if (progressList.children.length === 0) { - document.getElementById('transferProgress').classList.add('hidden'); - } -} diff --git a/web/static/js/p2p-transfer-new.js b/web/static/js/p2p-transfer-new.js deleted file mode 100644 index ffabcad..0000000 --- a/web/static/js/p2p-transfer-new.js +++ /dev/null @@ -1,1433 +0,0 @@ -// 全局变量 -let websocket = null; -let clientConnections = new Map(); // 存储与其他客户端的P2P连接 -let selectedFiles = []; -let currentPickupCode = ''; -let currentRole = ''; // 'sender' or 'receiver' -let currentClientId = ''; // 当前客户端ID -let fileTransfers = new Map(); // 存储文件传输状态 -let isP2PConnected = false; // P2P连接状态 -let isConnecting = false; // 是否正在连接中 -let pendingChunkMeta = null; // 待处理的数据块元数据 - -// 通知系统 -function showNotification(message, type = 'info', duration = 5000) { - // 移除现有通知 - const existing = document.querySelector('.notification'); - if (existing) { - existing.remove(); - } - - const notification = document.createElement('div'); - notification.className = `notification ${type}`; - - const icons = { - success: ` - - `, - error: ` - - `, - warning: ` - - `, - info: ` - - ` - }; - - notification.innerHTML = ` -
- ${icons[type]} - ${message} - -
- `; - - document.body.appendChild(notification); - - // 动画显示 - setTimeout(() => notification.classList.add('show'), 100); - - // 自动消失 - if (duration > 0) { - setTimeout(() => { - notification.classList.remove('show'); - setTimeout(() => notification.remove(), 300); - }, duration); - } -} - -// 复制取件码增强 -function copyPickupCode(event) { - // 阻止事件冒泡 - if (event) { - event.stopPropagation(); - event.preventDefault(); - } - - const code = document.getElementById('pickupCodeDisplay').textContent; - navigator.clipboard.writeText(code).then(() => { - showNotification('取件码已复制到剪贴板!', 'success', 3000); - - // 添加视觉反馈 - const codeDisplay = document.getElementById('pickupCodeDisplay'); - const originalText = codeDisplay.textContent; - codeDisplay.textContent = '✅ 已复制'; - codeDisplay.classList.add('success-bounce'); - - setTimeout(() => { - codeDisplay.textContent = originalText; - codeDisplay.classList.remove('success-bounce'); - }, 1500); - }).catch(() => { - showNotification('复制失败,请手动复制取件码', 'error'); - }); -} - -// 复制取件链接 -function copyPickupLink(event) { - // 阻止事件冒泡 - if (event) { - event.stopPropagation(); - event.preventDefault(); - } - - const link = document.getElementById('pickupLinkDisplay').textContent; - navigator.clipboard.writeText(link).then(() => { - showNotification('取件链接已复制到剪贴板!', 'success', 3000); - - // 添加视觉反馈 - const linkDisplay = document.getElementById('pickupLinkDisplay'); - const originalText = linkDisplay.textContent; - linkDisplay.textContent = '✅ 已复制'; - linkDisplay.classList.add('success-bounce'); - - setTimeout(() => { - linkDisplay.textContent = originalText; - linkDisplay.classList.remove('success-bounce'); - }, 1500); - }).catch(() => { - showNotification('复制失败,请手动复制链接', 'error'); - }); -} - -// 页面加载完成后初始化 -document.addEventListener('DOMContentLoaded', () => { - initializeEventListeners(); - initializeAnimations(); - handleUrlParams(); // 处理URL参数 -}); - -// 标签页切换函数 -function switchTab(tab) { - // 移除所有标签页的活动状态 - document.querySelectorAll('.tab-button').forEach(btn => { - btn.classList.remove('active', 'border-blue-500', 'bg-blue-50', 'text-blue-600', 'border-green-500', 'bg-green-50', 'text-green-600'); - btn.classList.add('border-transparent', 'text-gray-600'); - }); - - // 隐藏所有标签页内容 - document.querySelectorAll('.tab-content').forEach(content => { - content.classList.add('hidden'); - content.classList.remove('active'); - }); - - // 激活选中的标签页 - if (tab === 'send') { - const sendTab = document.getElementById('sendTab'); - const sendContent = document.getElementById('sendContent'); - - sendTab.classList.remove('border-transparent', 'text-gray-600'); - sendTab.classList.add('active', 'border-blue-500', 'bg-blue-50', 'text-blue-600'); - - sendContent.classList.remove('hidden'); - sendContent.classList.add('active'); - } else if (tab === 'receive') { - const receiveTab = document.getElementById('receiveTab'); - const receiveContent = document.getElementById('receiveContent'); - - receiveTab.classList.remove('border-transparent', 'text-gray-600'); - receiveTab.classList.add('active', 'border-green-500', 'bg-green-50', 'text-green-600'); - - receiveContent.classList.remove('hidden'); - receiveContent.classList.add('active'); - } -} - -// 处理URL参数 -function handleUrlParams() { - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get('code'); - - if (code && code.length === 6) { - // 切换到接收标签页 - switchTab('receive'); - - // 自动填入取件码 - const codeInput = document.getElementById('pickupCodeInput'); - codeInput.value = code.toUpperCase(); - - // 触发输入事件以应用样式 - codeInput.dispatchEvent(new Event('input')); - - // 显示通知并自动连接 - showNotification('检测到取件码,正在自动连接...', 'info', 3000); - setTimeout(() => { - joinRoom(); - }, 1000); - } -} - -// 初始化动画效果 -function initializeAnimations() { - // 为主要元素添加进入动画 - const leftPanel = document.querySelector('.lg\\:grid-cols-2 > div:first-child'); - const rightPanel = document.querySelector('.lg\\:grid-cols-2 > div:last-child'); - - if (leftPanel) { - leftPanel.classList.add('slide-in-left'); - } - - if (rightPanel) { - rightPanel.classList.add('slide-in-right'); - } - - // 标题动画 - const title = document.querySelector('h1'); - if (title) { - title.classList.add('fade-in-down'); - } - - // 为按钮添加点击反馈效果 - const buttons = document.querySelectorAll('button'); - buttons.forEach(button => { - button.classList.add('click-feedback'); - - // 添加悬停音效反馈(视觉) - button.addEventListener('mouseenter', () => { - if (!button.disabled) { - button.style.transform = 'translateY(-1px) scale(1.02)'; - } - }); - - button.addEventListener('mouseleave', () => { - button.style.transform = ''; - }); - }); -} - -// 初始化事件监听器 -function initializeEventListeners() { - // 文件选择事件 - document.getElementById('fileInput').addEventListener('change', handleFileSelect); - - // 取件码输入事件 - 增强用户体验 - const codeInput = document.getElementById('pickupCodeInput'); - codeInput.addEventListener('input', (e) => { - // 只允许字母和数字,自动转大写 - let value = e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase(); - e.target.value = value; - - // 视觉反馈 - if (value.length > 0) { - e.target.classList.remove('border-gray-200'); - e.target.classList.add('border-blue-300'); - } else { - e.target.classList.add('border-gray-200'); - e.target.classList.remove('border-blue-300'); - } - - // 长度验证和自动连接 - if (value.length === 6) { - e.target.classList.remove('border-blue-300'); - e.target.classList.add('border-green-400'); - showNotification('取件码格式正确,正在连接...', 'info', 3000); - // 自动连接 - setTimeout(() => joinRoom(), 500); - } else if (value.length > 6) { - e.target.value = value.substring(0, 6); - } - }); - - // 取件码输入框焦点事件 - codeInput.addEventListener('focus', () => { - codeInput.classList.add('ring-4', 'ring-blue-100'); - }); - - codeInput.addEventListener('blur', () => { - codeInput.classList.remove('ring-4', 'ring-blue-100'); - }); - - // 回车键快速连接 - codeInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter' && e.target.value.length === 6) { - joinRoom(); - } - }); - - // 拖拽上传 - setupDragAndDrop(); -} - -// 设置拖拽上传 -function setupDragAndDrop() { - const dropArea = document.getElementById('fileDropZone'); - - dropArea.addEventListener('dragover', (e) => { - e.preventDefault(); - dropArea.classList.add('drag-over'); - }); - - dropArea.addEventListener('dragenter', (e) => { - e.preventDefault(); - dropArea.classList.add('drag-over'); - }); - - dropArea.addEventListener('dragleave', (e) => { - e.preventDefault(); - // 只有当鼠标离开dropArea本身时才移除样式 - if (!dropArea.contains(e.relatedTarget)) { - dropArea.classList.remove('drag-over'); - } - }); - - dropArea.addEventListener('drop', (e) => { - e.preventDefault(); - dropArea.classList.remove('drag-over'); - - const files = Array.from(e.dataTransfer.files); - if (files.length > 0) { - // 添加新文件到现有列表 - selectedFiles = [...selectedFiles, ...files]; - displaySelectedFiles(); - - // 显示成功动画 - dropArea.classList.add('success-bounce'); - setTimeout(() => { - dropArea.classList.remove('success-bounce'); - }, 1000); - - // 如果已经生成了取件码,自动更新房间文件列表 - if (currentPickupCode && currentRole === 'sender') { - updateRoomFiles(); - } - } - }); -} - -// 处理文件选择 -function handleFileSelect(event) { - const files = Array.from(event.target.files); - if (files.length > 0) { - // 添加新文件到现有列表 - selectedFiles = [...selectedFiles, ...files]; - displaySelectedFiles(); - - // 如果已经生成了取件码,自动更新房间文件列表 - if (currentPickupCode && currentRole === 'sender') { - updateRoomFiles(); - } - } -} - -// 显示选中的文件 - 修改布局逻辑 -function displaySelectedFiles() { - console.log('displaySelectedFiles called, selectedFiles count:', selectedFiles.length); - - const fileDropZone = document.getElementById('fileDropZone'); - const fileListArea = document.getElementById('fileListArea'); - const filesList = document.getElementById('filesList'); - const fileCount = document.getElementById('fileCount'); - - console.log('Elements found:', { - fileDropZone: !!fileDropZone, - fileListArea: !!fileListArea, - filesList: !!filesList, - fileCount: !!fileCount - }); - - if (selectedFiles.length === 0) { - fileDropZone.style.display = 'block'; - fileListArea.classList.add('hidden'); - return; - } - - // 隐藏初始选择区域,显示文件列表区域 - fileDropZone.style.display = 'none'; - fileListArea.classList.remove('hidden'); - fileListArea.classList.add('fade-in-up'); - - // 更新文件计数 - if (fileCount) { - fileCount.textContent = `${selectedFiles.length} 个文件`; - } - - filesList.innerHTML = ''; - - selectedFiles.forEach((file, index) => { - const fileItem = document.createElement('div'); - fileItem.className = 'file-item flex items-center justify-between bg-gray-50 p-2 rounded-lg border hover:shadow-sm'; - - // 安全地获取文件信息 - const fileType = file.type || 'application/octet-stream'; - const fileName = file.name || '未知文件'; - const fileSize = file.size || 0; - - fileItem.innerHTML = ` -
-
- ${getFileIcon(fileType)} -
-
-
${fileName}
-
${formatFileSize(fileSize)}
-
-
- - `; - filesList.appendChild(fileItem); - }); -} - -// 处理拖拽区域点击 -function handleDropZoneClick(event) { - event.stopPropagation(); - document.getElementById('fileInput').click(); -} - -// 添加更多文件 -function addMoreFiles() { - document.getElementById('fileInput').click(); -} - -// 移除文件 -function removeFile(index, event) { - // 阻止事件冒泡 - if (event) { - event.stopPropagation(); - event.preventDefault(); - } - - selectedFiles.splice(index, 1); - - // 如果没有文件了,回到初始选择状态 - if (selectedFiles.length === 0) { - const fileDropZone = document.getElementById('fileDropZone'); - const fileListArea = document.getElementById('fileListArea'); - - fileDropZone.style.display = 'block'; - fileListArea.classList.add('hidden'); - } else { - displaySelectedFiles(); - } - - // 如果已经生成了取件码,需要更新房间文件列表 - if (currentPickupCode && currentRole === 'sender') { - updateRoomFiles(); - } -} - -// 添加更多文件 -function addMoreFiles() { - document.getElementById('fileInput').click(); -} - -// 更新房间文件列表 -async function updateRoomFiles() { - if (!currentPickupCode || currentRole !== 'sender') return; - - const fileInfos = selectedFiles.map((file, index) => ({ - id: 'file_' + index, - name: file.name, - size: file.size, - type: file.type, - lastModified: file.lastModified - })); - - try { - const response = await fetch('/api/update-room-files', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - code: currentPickupCode, - files: fileInfos - }) - }); - - const data = await response.json(); - if (data.success) { - console.log('房间文件列表已更新'); - showNotification('文件列表已更新', 'success'); - - // 通过WebSocket通知所有接收方文件列表更新 - if (websocket && websocket.readyState === WebSocket.OPEN) { - const updateMsg = { - type: 'file-list-updated', - payload: { - files: fileInfos - } - }; - websocket.send(JSON.stringify(updateMsg)); - } - } else { - console.error('更新文件列表失败:', data.message); - showNotification('更新文件列表失败: ' + data.message, 'error'); - } - } catch (error) { - console.error('更新文件列表请求失败:', error); - showNotification('更新文件列表失败,请重试', 'error'); - } -} - -// 生成取件码 -async function generatePickupCode() { - if (selectedFiles.length === 0) return; - - // 准备文件信息 - const fileInfos = selectedFiles.map((file, index) => ({ - id: 'file_' + index, - name: file.name, - size: file.size, - type: file.type, - lastModified: file.lastModified - })); - - try { - const response = await fetch('/api/create-room', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ files: fileInfos }) - }); - - const data = await response.json(); - if (data.success) { - currentPickupCode = data.code; - currentRole = 'sender'; - showPickupCode(data.code); - connectWebSocket(); - } else { - alert('生成取件码失败: ' + data.message); - } - } catch (error) { - console.error('生成取件码失败:', error); - alert('生成取件码失败,请重试'); - } -} - -// 显示取件码和链接 -function showPickupCode(code) { - const pickupCodeDisplay = document.getElementById('pickupCodeDisplay'); - const pickupLinkDisplay = document.getElementById('pickupLinkDisplay'); - - pickupCodeDisplay.textContent = code; - - // 生成特定链接 - const baseUrl = window.location.origin; - const pickupLink = `${baseUrl}/?code=${code}`; - pickupLinkDisplay.textContent = pickupLink; - - document.getElementById('pickupCodeSection').classList.remove('hidden'); - // 不隐藏生成取件码按钮,改为"添加更多文件" - const generateBtn = document.getElementById('generateCodeBtn'); - generateBtn.textContent = '➕ 添加更多文件'; - generateBtn.onclick = addMoreFiles; -} - -// 重置发送方 -function resetSender(event) { - // 阻止事件冒泡 - if (event) { - event.stopPropagation(); - event.preventDefault(); - } - - selectedFiles = []; - currentPickupCode = ''; - currentRole = ''; - currentClientId = ''; - if (websocket) { - websocket.close(); - } - - // 重置界面 - const fileDropZone = document.getElementById('fileDropZone'); - const fileListArea = document.getElementById('fileListArea'); - const pickupCodeSection = document.getElementById('pickupCodeSection'); - const generateBtn = document.getElementById('generateCodeBtn'); - const fileInput = document.getElementById('fileInput'); - const roomStatusSection = document.getElementById('roomStatusSection'); - - // 显示初始选择区域 - fileDropZone.style.display = 'block'; - fileListArea.classList.add('hidden'); - pickupCodeSection.classList.add('hidden'); - roomStatusSection.classList.add('hidden'); - - // 重置按钮 - generateBtn.textContent = '生成取件码'; - generateBtn.onclick = generatePickupCode; - - // 清空文件输入 - fileInput.value = ''; - - showNotification('已重置,可以重新选择文件', 'info', 2000); -} - -// 加入房间 -async function joinRoom() { - const codeInput = document.getElementById('pickupCodeInput'); - const code = codeInput.value.trim(); - const joinButton = document.querySelector('button[onclick="joinRoom()"]'); - - // 输入验证 - if (code.length !== 6) { - showNotification('请输入6位取件码', 'warning'); - codeInput.classList.add('error-shake'); - codeInput.focus(); - setTimeout(() => codeInput.classList.remove('error-shake'), 500); - return; - } - - // 防止重复点击 - if (isConnecting) { - return; - } - - isConnecting = true; - joinButton.disabled = true; - joinButton.classList.add('loading'); - const originalText = joinButton.textContent; - joinButton.textContent = '连接中...'; - - try { - showNotification('正在验证取件码...', 'info', 3000); - - const response = await fetch(`/api/room-info?code=${code}`); - const data = await response.json(); - - if (data.success) { - currentPickupCode = code; - currentRole = 'receiver'; - - showNotification('取件码验证成功!正在获取文件列表...', 'success', 3000); - - displayReceiverFiles(data.files); - connectWebSocket(); - - // 隐藏输入界面 - document.getElementById('codeInputSection').classList.add('hidden'); - } else { - showNotification(data.message || '取件码无效或已过期', 'error'); - codeInput.classList.add('error-shake'); - setTimeout(() => codeInput.classList.remove('error-shake'), 500); - } - } catch (error) { - console.error('连接失败:', error); - showNotification('连接失败,请检查网络连接或稍后重试', 'error'); - codeInput.classList.add('error-shake'); - setTimeout(() => codeInput.classList.remove('error-shake'), 500); - } finally { - isConnecting = false; - joinButton.disabled = false; - joinButton.classList.remove('loading'); - joinButton.textContent = originalText; - } -} - -// WebSocket连接函数 -function connectWebSocket() { - console.log('尝试连接WebSocket, 角色:', currentRole, '取件码:', currentPickupCode); - - if (!currentPickupCode || !currentRole) { - console.error('缺少必要参数:取件码或角色'); - showNotification('连接参数错误', 'error'); - return; - } - - if (isConnecting) { - console.log('已在连接中,跳过'); - return; - } - - isConnecting = true; - - // 如果已经有连接,先关闭 - if (websocket) { - console.log('关闭现有WebSocket连接'); - websocket.close(); - websocket = null; - } - - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${wsProtocol}//${window.location.host}/ws/p2p?code=${currentPickupCode}&role=${currentRole}`; - console.log('WebSocket URL:', wsUrl); - - try { - websocket = new WebSocket(wsUrl); - - websocket.onopen = () => { - console.log('WebSocket连接已建立, 当前角色:', currentRole); - isConnecting = false; - updateConnectionStatus(true); - - // 连接建立后,启用P2P功能 - if (currentRole === 'receiver') { - console.log('接收方WebSocket连接成功,启用下载功能'); - updateP2PStatus(true); // 接收方连接成功后立即启用下载 - showNotification('连接成功,可以开始下载文件', 'success'); - } - - // 发送方在WebSocket连接建立后显示房间状态 - if (currentRole === 'sender') { - console.log('发送方初始化完成'); - showRoomStatus(); - } - }; - - websocket.onmessage = async (event) => { - try { - const message = JSON.parse(event.data); - console.log('收到WebSocket消息:', message); - await handleWebSocketMessage(message); - } catch (error) { - console.error('解析WebSocket消息失败:', error, event.data); - } - }; - - websocket.onerror = (error) => { - console.error('WebSocket错误:', error); - isConnecting = false; - updateConnectionStatus(false); - updateP2PStatus(false); - showNotification('WebSocket连接失败,请检查网络连接', 'error'); - }; - - websocket.onclose = (event) => { - console.log('WebSocket连接已关闭, 代码:', event.code, '原因:', event.reason); - isConnecting = false; - updateConnectionStatus(false); - updateP2PStatus(false); - websocket = null; - - // 如果不是正常关闭且还需要连接,尝试重连 - if (event.code !== 1000 && currentPickupCode && !isConnecting) { - console.log('WebSocket异常关闭,5秒后尝试重连'); - showNotification('连接断开,5秒后自动重连...', 'info'); - setTimeout(() => { - if (currentPickupCode && !websocket && !isConnecting) { - console.log('尝试重新连接WebSocket'); - connectWebSocket(); - } - }, 5000); - } - }; - - // 设置连接超时 - setTimeout(() => { - if (websocket && websocket.readyState === WebSocket.CONNECTING) { - console.log('WebSocket连接超时'); - websocket.close(); - showNotification('连接超时,请重试', 'error'); - } - }, 10000); - - } catch (error) { - console.error('创建WebSocket连接失败:', error); - isConnecting = false; - showNotification('无法创建WebSocket连接: ' + error.message, 'error'); - } -} - -// 处理WebSocket消息 -async function handleWebSocketMessage(message) { - console.log('处理WebSocket消息:', message.type, message); - - switch (message.type) { - case 'file-list': - // 接收到文件列表 - if (currentRole === 'receiver') { - displayReceiverFiles(message.payload.files); - } - break; - - case 'file-list-updated': - // 文件列表更新(通知接收方) - if (currentRole === 'receiver') { - console.log('收到文件列表更新通知'); - displayReceiverFiles(message.payload.files); - showNotification('文件列表已更新,发现新文件!', 'info'); - } - break; - - case 'room-status': - // 房间状态更新 - updateRoomStatus(message.payload); - break; - - case 'new-receiver': - // 新接收方加入 - if (currentRole === 'sender') { - console.log('新接收方加入:', message.payload.client_id); - showNotification('有新用户加入房间', 'info'); - } - break; - - case 'new-sender': - // 新发送方加入 - if (currentRole === 'receiver') { - console.log('新发送方加入:', message.payload.client_id); - } - break; - - case 'client-left': - // 客户端离开 - console.log('客户端离开:', message.payload.client_id, message.payload.role); - break; - - case 'file-request': - // 文件请求 - if (currentRole === 'sender') { - await handleFileRequest(message.payload); - } - break; - - case 'file-info': - // 文件信息(接收方) - if (currentRole === 'receiver') { - initFileTransfer(message.payload); - } - break; - - case 'file-chunk': - // 文件数据块(接收方) - if (currentRole === 'receiver') { - receiveFileChunk(message.payload); - } - break; - - case 'file-complete': - // 文件传输完成(接收方) - if (currentRole === 'receiver') { - completeFileDownload(message.payload.file_id); - } - break; - - default: - console.log('未知消息类型:', message.type); - } -} - -// 更新连接状态 -function updateConnectionStatus(connected) { - const senderStatus = document.getElementById('senderStatus'); - const receiverStatus = document.getElementById('receiverStatus'); - - if (currentRole === 'sender' && senderStatus) { - senderStatus.innerHTML = connected ? - `
- - WebSocket已连接 -
` : - `
- - 连接断开 -
`; - } - - if (currentRole === 'receiver' && receiverStatus) { - // 接收方的状态更新由updateP2PStatus处理 - } -} - -// 更新房间状态显示 -function updateRoomStatus(status) { - console.log('更新房间状态:', status); - - const totalClients = status.sender_count + status.receiver_count; - - // 更新发送方界面的房间状态 - if (currentRole === 'sender') { - const onlineCountEl = document.getElementById('onlineCount'); - const senderCountEl = document.getElementById('senderCount'); - const receiverCountEl = document.getElementById('receiverCount'); - - if (onlineCountEl) onlineCountEl.textContent = totalClients; - if (senderCountEl) senderCountEl.textContent = status.sender_count; - if (receiverCountEl) receiverCountEl.textContent = status.receiver_count; - - const clientsList = document.getElementById('clientsList'); - if (clientsList) { - clientsList.innerHTML = ''; - - status.clients.forEach(client => { - if (client.id !== currentClientId) { // 不显示自己 - const clientDiv = document.createElement('div'); - clientDiv.className = 'text-xs text-blue-600'; - const role = client.role === 'sender' ? '📤 发送' : '📥 接收'; - const joinTime = new Date(client.joined_at).toLocaleTimeString(); - clientDiv.textContent = `${role} - ${joinTime}`; - clientsList.appendChild(clientDiv); - } - }); - } - - // 显示房间状态区域 - const roomStatusSection = document.getElementById('roomStatusSection'); - if (roomStatusSection) { - roomStatusSection.classList.remove('hidden'); - } - } - - // 更新接收方界面的房间状态 - if (currentRole === 'receiver') { - const receiverOnlineCountEl = document.getElementById('receiverOnlineCount'); - const receiverSenderCountEl = document.getElementById('receiverSenderCount'); - const receiverReceiverCountEl = document.getElementById('receiverReceiverCount'); - - if (receiverOnlineCountEl) receiverOnlineCountEl.textContent = totalClients; - if (receiverSenderCountEl) receiverSenderCountEl.textContent = status.sender_count; - if (receiverReceiverCountEl) receiverReceiverCountEl.textContent = status.receiver_count; - - const clientsList = document.getElementById('receiverClientsList'); - if (clientsList) { - clientsList.innerHTML = ''; - - status.clients.forEach(client => { - if (client.id !== currentClientId) { // 不显示自己 - const clientDiv = document.createElement('div'); - clientDiv.className = 'text-xs text-blue-600'; - const role = client.role === 'sender' ? '📤 发送' : '📥 接收'; - const joinTime = new Date(client.joined_at).toLocaleTimeString(); - clientDiv.textContent = `${role} - ${joinTime}`; - clientsList.appendChild(clientDiv); - } - }); - } - } -} - -// 显示房间状态区域 -function showRoomStatus() { - if (currentRole === 'sender') { - const roomStatusSection = document.getElementById('roomStatusSection'); - if (roomStatusSection) { - roomStatusSection.classList.remove('hidden'); - } - } -} - -// 处理文件请求(简化版本,通过WebSocket发送文件) -async function handleFileRequest(payload) { - console.log('处理文件请求:', payload); - - const fileId = payload.file_id; - const requesterId = payload.requester; - const requestId = payload.request_id; - - // 找到对应的文件 - const fileIndex = parseInt(fileId.replace('file_', '')); - const file = selectedFiles[fileIndex]; - - if (!file) { - console.error('未找到请求的文件:', fileId); - return; - } - - console.log('开始发送文件:', file.name, '给客户端:', requesterId); - showNotification(`开始发送文件: ${file.name}`, 'info'); - - // 通过WebSocket发送文件(简化实现) - await sendFileViaWebSocket(file, requestId); -} - -// 通过WebSocket发送文件 -async function sendFileViaWebSocket(file, requestId) { - // 发送文件信息 - const fileInfo = { - type: 'file-info', - payload: { - file_id: requestId, - name: file.name, - size: file.size, - mime_type: file.type, - last_modified: file.lastModified - } - }; - - websocket.send(JSON.stringify(fileInfo)); - - // 分块发送文件 - const chunkSize = 65536; // 64KB chunks (提高传输速度) - let offset = 0; - - const sendChunk = () => { - if (offset >= file.size) { - // 发送完成消息 - const completeMsg = { - type: 'file-complete', - payload: { - file_id: requestId - } - }; - websocket.send(JSON.stringify(completeMsg)); - console.log('文件发送完成:', file.name); - showNotification(`文件发送完成: ${file.name}`, 'success'); - return; - } - - const slice = file.slice(offset, offset + chunkSize); - const reader = new FileReader(); - - reader.onload = (e) => { - const chunk = e.target.result; - - // 发送块元数据和数据 - const chunkData = { - type: 'file-chunk', - payload: { - file_id: requestId, - offset: offset, - data: Array.from(new Uint8Array(chunk)), // 转换为数组以便JSON序列化 - is_last: offset + chunk.byteLength >= file.size - } - }; - - websocket.send(JSON.stringify(chunkData)); - - offset += chunk.byteLength; - - // 减少延时提高传输速度 - setTimeout(sendChunk, 10); // 从50ms减少到10ms - }; - - reader.readAsArrayBuffer(slice); - }; - - sendChunk(); -} - -// 初始化文件传输(接收方) -function initFileTransfer(fileInfo) { - console.log('初始化文件传输:', fileInfo); - - const transferKey = fileInfo.file_id; - - if (!fileTransfers.has(transferKey)) { - fileTransfers.set(transferKey, { - fileId: fileInfo.file_id, - chunks: [], - totalSize: fileInfo.size, - receivedSize: 0, - fileName: fileInfo.name, - mimeType: fileInfo.mime_type, - startTime: Date.now() - }); - - console.log('文件传输已初始化:', transferKey); - showTransferProgress(fileInfo.file_id, 'downloading', fileInfo.name); - } -} - -// 接收文件数据块(接收方) -function receiveFileChunk(chunkData) { - const transferKey = chunkData.file_id; - const transfer = fileTransfers.get(transferKey); - - if (!transfer) { - console.error('未找到对应的文件传输:', transferKey); - return; - } - - // 将数组转换回Uint8Array - const chunkArray = new Uint8Array(chunkData.data); - - // 存储数据块 - transfer.chunks.push({ - offset: chunkData.offset, - data: chunkArray - }); - - transfer.receivedSize += chunkArray.length; - - // 更新进度 - const progress = (transfer.receivedSize / transfer.totalSize) * 100; - updateTransferProgress(chunkData.file_id, progress, transfer.receivedSize, transfer.totalSize); - - console.log(`文件块接收进度: ${progress.toFixed(1)}% (${transfer.receivedSize}/${transfer.totalSize})`); - - // 检查是否是最后一块 - if (chunkData.is_last || transfer.receivedSize >= transfer.totalSize) { - console.log('文件接收完成,开始合并数据块'); - assembleAndDownloadFile(transferKey); - } -} - -// 完成文件下载(接收方) -function completeFileDownload(fileId) { - console.log('文件传输完成:', fileId); - // 这个函数可能不需要,因为在receiveFileChunk中已经处理了完成逻辑 -} - -// 组装文件并触发下载 -function assembleAndDownloadFile(transferKey) { - const transfer = fileTransfers.get(transferKey); - if (!transfer) { - console.error('未找到文件传输信息:', transferKey); - return; - } - - // 按偏移量排序数据块 - transfer.chunks.sort((a, b) => a.offset - b.offset); - - // 合并所有数据块 - const totalSize = transfer.chunks.reduce((sum, chunk) => sum + chunk.data.length, 0); - const mergedData = new Uint8Array(totalSize); - let currentOffset = 0; - - transfer.chunks.forEach(chunk => { - mergedData.set(chunk.data, currentOffset); - currentOffset += chunk.data.length; - }); - - // 创建Blob并触发下载 - const blob = new Blob([mergedData], { type: transfer.mimeType }); - - // 创建下载链接 - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = transfer.fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - // 清理传输信息 - fileTransfers.delete(transferKey); - - // 显示完成状态 - hideTransferProgress(transfer.fileId); - - // 恢复下载按钮 - const button = document.querySelector(`button[onclick="downloadFile('${transfer.fileId}')"]`); - if (button) { - button.disabled = false; - button.textContent = '📥 下载'; - } - - const transferTime = (Date.now() - transfer.startTime) / 1000; - const speed = (transfer.totalSize / transferTime / 1024 / 1024).toFixed(2); - - console.log(`文件下载完成: ${transfer.fileName}`); - console.log(`传输时间: ${transferTime.toFixed(1)}秒,平均速度: ${speed} MB/s`); - - // 显示成功消息 - showNotification(`文件 "${transfer.fileName}" 下载完成!传输速度: ${speed} MB/s`, 'success'); -} - -// 显示接收方文件列表 -function displayReceiverFiles(files) { - console.log('displayReceiverFiles被调用, WebSocket状态:', websocket ? websocket.readyState : 'null'); - - document.getElementById('codeInputSection').classList.add('hidden'); - document.getElementById('receiverFilesSection').classList.remove('hidden'); - - const filesList = document.getElementById('receiverFilesList'); - filesList.innerHTML = ''; - - files.forEach((file, index) => { - const fileItem = document.createElement('div'); - fileItem.className = 'file-item flex items-center justify-between bg-gray-50 p-2 rounded-lg border hover:shadow-sm transition-all'; - fileItem.innerHTML = ` -
-
- ${getFileIcon(file.type)} -
-
-
${file.name}
-
${formatFileSize(file.size)}
-
-
- - `; - filesList.appendChild(fileItem); - }); - - // 显示文件列表后,检查连接状态 - console.log('文件列表显示完成,当前WebSocket状态:', websocket ? websocket.readyState : 'null'); - - // 延迟一点检查状态,确保DOM更新完成 - setTimeout(() => { - if (websocket && websocket.readyState === WebSocket.OPEN) { - console.log('WebSocket已连接,启用下载功能'); - updateP2PStatus(true); - } else { - console.log('WebSocket未连接,显示连接中状态'); - updateP2PStatus(false); - } - }, 100); -} - -// 下载文件(多人房间版本) -function downloadFile(fileId) { - if (!websocket || websocket.readyState !== WebSocket.OPEN) { - alert('WebSocket连接未建立,请重新连接'); - return; - } - - console.log('请求下载文件:', fileId); - - // 找到文件名(从按钮的父元素中获取) - const button = document.querySelector(`button[onclick="downloadFile('${fileId}')"]`); - let fileName = fileId; // 默认使用fileId - if (button) { - const fileNameEl = button.parentElement.querySelector('.font-medium'); - if (fileNameEl) { - fileName = fileNameEl.textContent; - } - } - - // 生成请求ID用于跟踪请求 - const requestId = 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); - - // 通过WebSocket发送文件请求 - const request = { - type: 'file-request', - payload: { - file_id: fileId, - request_id: requestId - } - }; - - websocket.send(JSON.stringify(request)); - // 不在这里显示进度条,等收到file-info消息时再显示 - - // 禁用下载按钮防止重复点击 - if (button) { - button.disabled = true; - button.textContent = '⏳ 请求中...'; - } -} - -// 更新P2P连接状态 -function updateP2PStatus(connected) { - console.log('updateP2PStatus被调用, connected:', connected, 'currentRole:', currentRole); - - const receiverStatus = document.getElementById('receiverStatus'); - const downloadButtons = document.querySelectorAll('button[onclick^="downloadFile"]'); - - console.log('receiverStatus元素:', receiverStatus); - console.log('找到的下载按钮数量:', downloadButtons.length); - - if (currentRole === 'receiver' && receiverStatus) { - if (connected) { - console.log('设置为已连接状态'); - receiverStatus.innerHTML = ` -
-
-
- 已连接,可下载文件 -
-
`; - - // 启用下载按钮 - downloadButtons.forEach(btn => { - console.log('启用下载按钮:', btn); - btn.disabled = false; - btn.classList.remove('opacity-50', 'cursor-not-allowed'); - btn.classList.add('hover:bg-green-600'); - - // 更新按钮内容 - const svg = btn.querySelector('svg'); - if (svg) { - svg.innerHTML = ``; - } - const textNode = btn.childNodes[btn.childNodes.length - 1]; - if (textNode && textNode.nodeType === Node.TEXT_NODE) { - textNode.textContent = '下载'; - } - }); - } else { - console.log('设置为连接中状态'); - receiverStatus.innerHTML = ` -
-
-
- 正在建立连接... -
-
`; - - // 禁用下载按钮 - downloadButtons.forEach(btn => { - btn.disabled = true; - btn.classList.add('opacity-50', 'cursor-not-allowed'); - btn.classList.remove('hover:bg-green-600'); - - // 更新按钮内容为等待状态 - const svg = btn.querySelector('svg'); - if (svg) { - svg.innerHTML = ``; - } - const textNode = btn.childNodes[btn.childNodes.length - 1]; - if (textNode && textNode.nodeType === Node.TEXT_NODE) { - textNode.textContent = '等待连接'; - } - }); - } - } else { - console.log('条件不满足: currentRole=' + currentRole + ', receiverStatus存在=' + !!receiverStatus); - } -} - -// 显示传输进度 -function showTransferProgress(fileId, type, fileName = null) { - const progressContainer = document.getElementById('transferProgress'); - const progressList = document.getElementById('progressList'); - - if (!progressContainer || !progressList) return; - - // 如果已经存在相同文件ID的进度条,先删除 - const existingProgress = document.getElementById(`progress-${fileId}`); - if (existingProgress) { - existingProgress.remove(); - } - - progressContainer.classList.remove('hidden'); - progressContainer.classList.add('fade-in-up'); - - const displayName = fileName || fileId; - const progressItem = document.createElement('div'); - progressItem.id = `progress-${fileId}`; - progressItem.className = 'bg-white border border-gray-200 p-4 rounded-xl shadow-sm'; - progressItem.innerHTML = ` -
-
-
- - - -
-
-
${displayName}
-
${type === 'uploading' ? '正在发送' : '正在接收'}
-
-
-
-
0%
-
准备中...
-
-
-
-
-
- `; - - progressList.appendChild(progressItem); -} - -// 更新传输进度 -function updateTransferProgress(fileId, progress, received, total) { - const progressBar = document.getElementById(`progress-bar-${fileId}`); - const progressPercent = document.getElementById(`progress-percent-${fileId}`); - const progressSize = document.getElementById(`progress-size-${fileId}`); - - if (progressBar) { - progressBar.style.width = `${progress}%`; - } - - if (progressPercent) { - progressPercent.textContent = `${progress.toFixed(1)}%`; - } - - if (progressSize) { - progressSize.textContent = `${formatFileSize(received)} / ${formatFileSize(total)}`; - } -} - -// 隐藏传输进度 -function hideTransferProgress(fileId) { - const progressItem = document.getElementById(`progress-${fileId}`); - if (progressItem) { - progressItem.remove(); - - // 如果没有其他传输,隐藏进度容器 - const progressList = document.getElementById('progressList'); - if (progressList && progressList.children.length === 0) { - document.getElementById('transferProgress').classList.add('hidden'); - } - } -} - -// 显示通知 -function showNotification(message, type = 'info') { - // 创建通知元素 - const notification = document.createElement('div'); - notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 max-w-sm ${ - type === 'success' ? 'bg-green-500 text-white' : - type === 'error' ? 'bg-red-500 text-white' : - 'bg-blue-500 text-white' - }`; - notification.textContent = message; - - document.body.appendChild(notification); - - // 3秒后自动移除 - setTimeout(() => { - if (notification.parentNode) { - notification.parentNode.removeChild(notification); - } - }, 3000); -} - -// 工具函数 -function getFileIcon(mimeType) { - if (mimeType.startsWith('image/')) return '🖼️'; - if (mimeType.startsWith('video/')) return '🎥'; - if (mimeType.startsWith('audio/')) return '🎵'; - if (mimeType.includes('pdf')) return '📄'; - if (mimeType.includes('zip') || mimeType.includes('rar')) return '📦'; - return '📄'; -} - -function formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} - -// 页面卸载时清理 -window.addEventListener('beforeunload', () => { - if (websocket) { - websocket.close(); - } - clientConnections.forEach((conn) => { - if (conn.peerConnection) { - conn.peerConnection.close(); - } - }); -}); diff --git a/web/static/js/webrtc-connection.js b/web/static/js/webrtc-connection.js deleted file mode 100644 index 61abe5d..0000000 --- a/web/static/js/webrtc-connection.js +++ /dev/null @@ -1,902 +0,0 @@ -// WebSocket和WebRTC连接管理 - -// 全局变量 -let clientConnections = new Map(); // 存储与其他客户端的P2P连接 -let currentClientId = ''; // 当前客户端ID - -// WebSocket连接 -function connectWebSocket() { - console.log('尝试连接WebSocket, 角色:', currentRole, '取件码:', currentPickupCode); - - if (!currentPickupCode || !currentRole) { - console.error('缺少必要参数:取件码或角色'); - return; - } - - if (isConnecting) { - console.log('已在连接中,跳过'); - return; - } - - isConnecting = true; - - // 如果已经有连接,先关闭 - if (websocket) { - console.log('关闭现有WebSocket连接'); - websocket.close(); - websocket = null; - } - - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${wsProtocol}//${window.location.host}/ws/p2p?code=${currentPickupCode}&role=${currentRole}`; - console.log('WebSocket URL:', wsUrl); - - try { - websocket = new WebSocket(wsUrl); - - websocket.onopen = () => { - console.log('WebSocket连接已建立'); - isConnecting = false; - updateConnectionStatus(true); - - // 连接建立后,启用P2P功能 - if (currentRole === 'receiver') { - updateP2PStatus(true); // 接收方连接成功后立即启用下载 - } - - // 发送方在WebSocket连接建立后初始化(等待接收方连接) - if (currentRole === 'sender') { - console.log('发送方初始化完成,等待接收方连接'); - showRoomStatus(); - } - }; - - websocket.onmessage = async (event) => { - try { - const message = JSON.parse(event.data); - console.log('收到WebSocket消息:', message); - await handleWebSocketMessage(message); - } catch (error) { - console.error('解析WebSocket消息失败:', error, event.data); - } - }; - - websocket.onerror = (error) => { - console.error('WebSocket错误:', error); - isConnecting = false; - updateConnectionStatus(false); - updateP2PStatus(false); - showNotification('WebSocket连接失败,请检查网络连接', 'error'); - }; - - websocket.onclose = (event) => { - console.log('WebSocket连接已关闭, 代码:', event.code, '原因:', event.reason); - isConnecting = false; - updateConnectionStatus(false); - updateP2PStatus(false); - websocket = null; - - // 清理所有P2P连接 - clientConnections.forEach((conn, clientId) => { - if (conn.peerConnection) { - conn.peerConnection.close(); - } - }); - clientConnections.clear(); - - // 如果不是正常关闭且还需要连接,尝试重连 - if (event.code !== 1000 && currentPickupCode && !isConnecting) { - console.log('WebSocket异常关闭,5秒后尝试重连'); - showNotification('连接断开,5秒后自动重连...', 'info'); - setTimeout(() => { - if (currentPickupCode && !websocket && !isConnecting) { - console.log('尝试重新连接WebSocket'); - connectWebSocket(); - } - }, 5000); - } - }; - - // 设置连接超时 - setTimeout(() => { - if (websocket && websocket.readyState === WebSocket.CONNECTING) { - console.log('WebSocket连接超时'); - websocket.close(); - showNotification('连接超时,请重试', 'error'); - } - }, 10000); - - } catch (error) { - console.error('创建WebSocket连接失败:', error); - isConnecting = false; - showNotification('无法创建WebSocket连接', 'error'); - } -} - -// 处理WebSocket消息 -async function handleWebSocketMessage(message) { - console.log('处理WebSocket消息:', message.type, message); - - switch (message.type) { - case 'file-list': - // 接收到文件列表 - if (currentRole === 'receiver') { - displayReceiverFiles(message.payload.files); - } - break; - - case 'room-status': - // 房间状态更新 - updateRoomStatus(message.payload); - break; - - case 'new-receiver': - // 新接收方加入 - if (currentRole === 'sender') { - console.log('新接收方加入:', message.payload.client_id); - // 发送方可以准备为新接收方创建P2P连接 - } - break; - - case 'new-sender': - // 新发送方加入 - if (currentRole === 'receiver') { - console.log('新发送方加入:', message.payload.client_id); - } - break; - - case 'client-left': - // 客户端离开 - console.log('客户端离开:', message.payload.client_id, message.payload.role); - // 清理对应的P2P连接 - if (clientConnections.has(message.payload.client_id)) { - const conn = clientConnections.get(message.payload.client_id); - if (conn.peerConnection) { - conn.peerConnection.close(); - } - clientConnections.delete(message.payload.client_id); - } - break; - - case 'file-request': - // 文件请求 - if (currentRole === 'sender') { - await handleFileRequest(message.payload); - } - break; - - // WebRTC信令消息 - case 'offer': - await handleOffer(message.payload); - break; - case 'answer': - await handleAnswer(message.payload); - break; - case 'ice-candidate': - await handleIceCandidate(message.payload); - break; - - default: - console.log('未知消息类型:', message.type); - } -} - -// 更新房间状态显示 -function updateRoomStatus(status) { - console.log('更新房间状态:', status); - - const totalClients = status.sender_count + status.receiver_count; - - // 更新发送方界面的房间状态 - if (currentRole === 'sender') { - const onlineCountEl = document.getElementById('onlineCount'); - const senderCountEl = document.getElementById('senderCount'); - const receiverCountEl = document.getElementById('receiverCount'); - - if (onlineCountEl) onlineCountEl.textContent = totalClients; - if (senderCountEl) senderCountEl.textContent = status.sender_count; - if (receiverCountEl) receiverCountEl.textContent = status.receiver_count; - - const clientsList = document.getElementById('clientsList'); - if (clientsList) { - clientsList.innerHTML = ''; - - status.clients.forEach(client => { - if (client.id !== currentClientId) { // 不显示自己 - const clientDiv = document.createElement('div'); - clientDiv.className = 'text-xs text-blue-600'; - const role = client.role === 'sender' ? '📤 发送' : '📥 接收'; - const joinTime = new Date(client.joined_at).toLocaleTimeString(); - clientDiv.textContent = `${role} - ${joinTime}`; - clientsList.appendChild(clientDiv); - } - }); - } - - // 显示房间状态区域 - const roomStatusSection = document.getElementById('roomStatusSection'); - if (roomStatusSection) { - roomStatusSection.classList.remove('hidden'); - } - } - - // 更新接收方界面的房间状态 - if (currentRole === 'receiver') { - const receiverOnlineCountEl = document.getElementById('receiverOnlineCount'); - const receiverSenderCountEl = document.getElementById('receiverSenderCount'); - const receiverReceiverCountEl = document.getElementById('receiverReceiverCount'); - - if (receiverOnlineCountEl) receiverOnlineCountEl.textContent = totalClients; - if (receiverSenderCountEl) receiverSenderCountEl.textContent = status.sender_count; - if (receiverReceiverCountEl) receiverReceiverCountEl.textContent = status.receiver_count; - - const clientsList = document.getElementById('receiverClientsList'); - if (clientsList) { - clientsList.innerHTML = ''; - - status.clients.forEach(client => { - if (client.id !== currentClientId) { // 不显示自己 - const clientDiv = document.createElement('div'); - clientDiv.className = 'text-xs text-blue-600'; - const role = client.role === 'sender' ? '📤 发送' : '📥 接收'; - const joinTime = new Date(client.joined_at).toLocaleTimeString(); - clientDiv.textContent = `${role} - ${joinTime}`; - clientsList.appendChild(clientDiv); - } - }); - } - } -} - -// 显示房间状态区域 -function showRoomStatus() { - if (currentRole === 'sender') { - document.getElementById('roomStatusSection').classList.remove('hidden'); - } -} - -// 更新连接状态 -function updateConnectionStatus(connected) { - const senderStatus = document.getElementById('senderStatus'); - const receiverStatus = document.getElementById('receiverStatus'); - - if (currentRole === 'sender' && senderStatus) { - senderStatus.innerHTML = connected ? - `
- - WebSocket已连接 -
` : - `
- - 连接断开 -
`; - } - - if (currentRole === 'receiver' && receiverStatus) { - receiverStatus.innerHTML = connected ? - `
- - 已连接,可以下载文件 -
` : - `
- - 连接断开 -
`; - } -} - -// 处理文件请求 -async function handleFileRequest(payload) { - console.log('处理文件请求:', payload); - - const fileId = payload.file_id; - const requesterId = payload.requester; - const requestId = payload.request_id; - - // 找到对应的文件 - const file = selectedFiles.find(f => f.id === fileId || selectedFiles.indexOf(f).toString() === fileId); - if (!file) { - console.error('未找到请求的文件:', fileId); - return; - } - - // 创建或获取与请求者的P2P连接 - let connection = clientConnections.get(requesterId); - if (!connection) { - connection = await createPeerConnection(requesterId); - clientConnections.set(requesterId, connection); - } - - // 发送文件 - if (connection.dataChannel && connection.dataChannel.readyState === 'open') { - await sendFileToClient(file, connection.dataChannel, requestId); - } else { - console.log('等待数据通道建立...'); - connection.pendingFiles = connection.pendingFiles || []; - connection.pendingFiles.push({ file, requestId }); - } -} - -// 创建P2P连接 -async function createPeerConnection(targetClientId) { - console.log('创建P2P连接到:', targetClientId); - - const connection = { - peerConnection: null, - dataChannel: null, - pendingFiles: [] - }; - - const pc = new RTCPeerConnection({ - iceServers: [ - { urls: 'stun:stun.l.google.com:19302' }, - { urls: 'stun:stun1.l.google.com:19302' } - ] - }); - - connection.peerConnection = pc; - - // 创建数据通道(发送方) - if (currentRole === 'sender') { - const dataChannel = pc.createDataChannel('fileTransfer', { - ordered: true - }); - - connection.dataChannel = dataChannel; - - dataChannel.onopen = () => { - console.log('数据通道已打开,可以传输文件'); - // 发送待发送的文件 - if (connection.pendingFiles && connection.pendingFiles.length > 0) { - connection.pendingFiles.forEach(({ file, requestId }) => { - sendFileToClient(file, dataChannel, requestId); - }); - connection.pendingFiles = []; - } - }; - - dataChannel.onmessage = (event) => { - console.log('数据通道收到消息:', event.data); - }; - } - - // 处理数据通道(接收方) - pc.ondatachannel = (event) => { - const channel = event.channel; - connection.dataChannel = channel; - - channel.onopen = () => { - console.log('接收方数据通道已打开'); - }; - - channel.onmessage = (event) => { - handleFileData(event.data, targetClientId); - }; - }; - - // ICE候选者 - pc.onicecandidate = (event) => { - if (event.candidate) { - websocket.send(JSON.stringify({ - type: 'ice-candidate', - payload: { - candidate: event.candidate, - target_client: targetClientId - } - })); - } - }; - - return connection; -} - -// 处理WebRTC信令消息 -async function handleOffer(payload) { - console.log('处理offer:', payload); - // 实现WebRTC offer处理逻辑 -} - -async function handleAnswer(payload) { - console.log('处理answer:', payload); - // 实现WebRTC answer处理逻辑 -} - -// 发送文件给客户端 -async function sendFileToClient(file, dataChannel, requestId) { - console.log('开始发送文件:', file.name, '到客户端'); - - // 发送文件信息 - const fileInfo = { - type: 'file-info', - file_id: requestId, - name: file.name, - size: file.size, - mime_type: file.type, - last_modified: file.lastModified - }; - - dataChannel.send(JSON.stringify(fileInfo)); - - // 分块发送文件 - const chunkSize = 65536; // 64KB chunks - let offset = 0; - - const sendChunk = () => { - if (offset >= file.size) { - // 发送完成消息 - const completeMsg = { - type: 'file-complete', - file_id: requestId - }; - dataChannel.send(JSON.stringify(completeMsg)); - console.log('文件发送完成:', file.name); - return; - } - - const slice = file.slice(offset, offset + chunkSize); - const reader = new FileReader(); - - reader.onload = (e) => { - const chunk = e.target.result; - - // 发送块元数据 - const metadata = { - type: 'file-chunk-meta', - file_id: requestId, - offset: offset, - size: chunk.byteLength, - is_last: offset + chunk.byteLength >= file.size - }; - - dataChannel.send(JSON.stringify(metadata)); - - // 发送二进制数据 - dataChannel.send(chunk); - - offset += chunk.byteLength; - - // 继续发送下一块 - setTimeout(sendChunk, 10); // 小延时以避免阻塞 - }; - - reader.readAsArrayBuffer(slice); - }; - - sendChunk(); -} - -// 处理接收到的文件数据 -function handleFileData(data, senderId) { - console.log('从发送方接收文件数据:', senderId); - - // 检查是否是二进制数据 - if (data instanceof ArrayBuffer) { - // 处理二进制数据块 - if (pendingChunkMeta) { - receiveFileChunk(pendingChunkMeta, data, senderId); - pendingChunkMeta = null; - } - } else { - // 处理JSON消息 - try { - const message = JSON.parse(data); - console.log('接收到文件传输消息:', message.type); - - switch (message.type) { - case 'file-chunk-meta': - // 存储chunk元数据,等待二进制数据 - pendingChunkMeta = message; - break; - - case 'file-info': - // 初始化文件传输 - initFileTransfer(message, senderId); - break; - - case 'file-complete': - // 文件传输完成 - completeFileDownload(message.file_id, senderId); - break; - - default: - console.log('未知文件传输消息类型:', message.type); - } - } catch (error) { - console.error('解析文件传输消息失败:', error); - } - } -} - -// 初始化文件传输 -function initFileTransfer(fileInfo, senderId) { - console.log('初始化文件传输:', fileInfo); - - const transferKey = `${fileInfo.file_id}_${senderId}`; - - if (!fileTransfers.has(transferKey)) { - fileTransfers.set(transferKey, { - fileId: fileInfo.file_id, - senderId: senderId, - chunks: [], - totalSize: fileInfo.size, - receivedSize: 0, - fileName: fileInfo.name, - mimeType: fileInfo.mime_type || fileInfo.type, - startTime: Date.now() - }); - - console.log('文件传输已初始化:', transferKey); - } -} - -// 接收文件数据块 -function receiveFileChunk(metadata, chunk, senderId) { - const transferKey = `${metadata.file_id}_${senderId}`; - const transfer = fileTransfers.get(transferKey); - - if (!transfer) { - console.error('未找到对应的文件传输:', transferKey); - return; - } - - // 存储数据块 - transfer.chunks.push({ - offset: metadata.offset, - data: chunk - }); - - transfer.receivedSize += chunk.byteLength; - - // 更新进度 - const progress = (transfer.receivedSize / transfer.totalSize) * 100; - updateTransferProgress(metadata.file_id, progress, transfer.receivedSize, transfer.totalSize); - - console.log(`文件块接收进度: ${progress.toFixed(1)}% (${transfer.receivedSize}/${transfer.totalSize})`); - - // 检查是否是最后一块 - if (metadata.is_last || transfer.receivedSize >= transfer.totalSize) { - console.log('文件接收完成,开始合并数据块'); - assembleAndDownloadFile(transferKey); - } -} - -// 组装文件并触发下载 -function assembleAndDownloadFile(transferKey) { - const transfer = fileTransfers.get(transferKey); - if (!transfer) { - console.error('未找到文件传输信息:', transferKey); - return; - } - - // 按偏移量排序数据块 - transfer.chunks.sort((a, b) => a.offset - b.offset); - - // 合并所有数据块 - const totalSize = transfer.chunks.reduce((sum, chunk) => sum + chunk.data.byteLength, 0); - const mergedData = new Uint8Array(totalSize); - let currentOffset = 0; - - transfer.chunks.forEach(chunk => { - const chunkView = new Uint8Array(chunk.data); - mergedData.set(chunkView, currentOffset); - currentOffset += chunkView.length; - }); - - // 创建Blob并触发下载 - const blob = new Blob([mergedData], { type: transfer.mimeType }); - - // 创建下载链接 - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = transfer.fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - // 清理传输信息 - fileTransfers.delete(transferKey); - - // 显示完成状态 - hideTransferProgress(transfer.fileId); - - // 恢复下载按钮 - const button = document.querySelector(`button[onclick="downloadFile('${transfer.fileId}')"]`); - if (button) { - button.disabled = false; - button.textContent = '📥 下载'; - } - - const transferTime = (Date.now() - transfer.startTime) / 1000; - const speed = (transfer.totalSize / transferTime / 1024 / 1024).toFixed(2); - - console.log(`文件下载完成: ${transfer.fileName}`); - console.log(`传输时间: ${transferTime.toFixed(1)}秒,平均速度: ${speed} MB/s`); - - // 显示成功消息 - showNotification(`文件 "${transfer.fileName}" 下载完成!传输速度: ${speed} MB/s`, 'success'); -} - -// 为发送方初始化P2P连接(不立即创建offer) -function initPeerConnectionForSender() { - console.log('为发送方初始化P2P连接(等待接收方就绪)'); - - // 清除之前的超时定时器 - if (connectionTimeout) { - clearTimeout(connectionTimeout); - } - - // 设置连接超时(60秒,合理的超时时间) - connectionTimeout = setTimeout(() => { - console.error('P2P连接超时(60秒)'); - if (peerConnection && !isP2PConnected) { - console.log('关闭超时的P2P连接'); - peerConnection.close(); - peerConnection = null; - updateP2PStatus(false); - alert('P2P连接超时,请检查网络连接并重试'); - } - }, 60000); - - // 使用国内优化的WebRTC配置 - peerConnection = new RTCPeerConnection({ - iceServers: [ - // 阿里云和腾讯STUN服务器 - { urls: 'stun:stun.chat.bilibili.com:3478' }, - { urls: 'stun:stun.voipbuster.com' }, - { urls: 'stun:stun.voipstunt.com' }, - { urls: 'stun:stun.qq.com:3478' }, - // 备用国外服务器 - { urls: 'stun:stun.l.google.com:19302' }, - { urls: 'stun:stun1.l.google.com:19302' } - ], - iceCandidatePoolSize: 10 - }); - - // 连接状态监听 - peerConnection.onconnectionstatechange = () => { - console.log('P2P连接状态:', peerConnection.connectionState); - if (peerConnection.connectionState === 'connected') { - console.log('P2P连接建立成功'); - isP2PConnected = true; - updateP2PStatus(true); - - // 清除连接超时定时器 - if (connectionTimeout) { - clearTimeout(connectionTimeout); - connectionTimeout = null; - } - } else if (peerConnection.connectionState === 'failed') { - console.error('P2P连接失败'); - updateP2PStatus(false); - } - }; - - // ICE连接状态监听 - peerConnection.oniceconnectionstatechange = () => { - console.log('ICE连接状态:', peerConnection.iceConnectionState); - if (peerConnection.iceConnectionState === 'failed') { - console.error('ICE连接失败'); - updateP2PStatus(false); - } - }; - - // 创建数据通道 - dataChannel = peerConnection.createDataChannel('fileTransfer', { - ordered: true - }); - setupDataChannel(dataChannel); - - // 处理ICE候选 - peerConnection.onicecandidate = (event) => { - if (event.candidate) { - console.log('发送ICE候选:', event.candidate.candidate); - sendWebSocketMessage({ - type: 'ice-candidate', - payload: event.candidate - }); - } else { - console.log('ICE候选收集完成'); - } - }; -} - -// 创建offer(发送方专用) -function createOffer() { - if (!peerConnection) { - console.error('PeerConnection未初始化'); - return; - } - - console.log('发送方创建 offer'); - - peerConnection.createOffer().then(offer => { - console.log('Offer 创建成功'); - return peerConnection.setLocalDescription(offer); - }).then(() => { - console.log('本地描述设置成功,发送 offer'); - sendWebSocketMessage({ - type: 'offer', - payload: peerConnection.localDescription - }); - }).catch(error => { - console.error('创建 offer 失败:', error); - }); -} - -// 初始化P2P连接(接收方使用) -function initPeerConnection() { - console.log('接收方初始化P2P连接'); - - // 清除之前的超时定时器 - if (connectionTimeout) { - clearTimeout(connectionTimeout); - } - - // 设置连接超时(60秒) - connectionTimeout = setTimeout(() => { - console.error('P2P连接超时(60秒)'); - if (peerConnection && !isP2PConnected) { - console.log('关闭超时的P2P连接'); - peerConnection.close(); - peerConnection = null; - updateP2PStatus(false); - alert('P2P连接超时,请检查网络连接并重试'); - } - }, 60000); - - // 使用国内优化配置 - peerConnection = new RTCPeerConnection({ - iceServers: [ - // 阿里云和腾讯STUN服务器 - { urls: 'stun:stun.chat.bilibili.com:3478' }, - { urls: 'stun:stun.voipbuster.com' }, - { urls: 'stun:stun.voipstunt.com' }, - { urls: 'stun:stun.qq.com:3478' }, - // 备用国外服务器 - { urls: 'stun:stun.l.google.com:19302' }, - { urls: 'stun:stun1.l.google.com:19302' } - ], - iceCandidatePoolSize: 10 - }); - - // 连接状态监听 - peerConnection.onconnectionstatechange = () => { - console.log('P2P连接状态:', peerConnection.connectionState); - if (peerConnection.connectionState === 'connected') { - console.log('P2P连接建立成功'); - isP2PConnected = true; - updateP2PStatus(true); - - // 清除连接超时定时器 - if (connectionTimeout) { - clearTimeout(connectionTimeout); - connectionTimeout = null; - } - } else if (peerConnection.connectionState === 'failed') { - console.error('P2P连接失败'); - updateP2PStatus(false); - } - }; - - // ICE连接状态监听 - peerConnection.oniceconnectionstatechange = () => { - console.log('ICE连接状态:', peerConnection.iceConnectionState); - if (peerConnection.iceConnectionState === 'failed') { - console.error('ICE连接失败'); - updateP2PStatus(false); - } - }; - - // 处理数据通道 - peerConnection.ondatachannel = (event) => { - console.log('接收到数据通道'); - const channel = event.channel; - setupDataChannel(channel); - }; - - // 处理ICE候选 - peerConnection.onicecandidate = (event) => { - if (event.candidate) { - console.log('发送ICE候选:', event.candidate.candidate); - sendWebSocketMessage({ - type: 'ice-candidate', - payload: event.candidate - }); - } else { - console.log('ICE候选收集完成'); - } - }; -} - -// 处理WebSocket消息 -async function handleWebSocketMessage(message) { - console.log('收到WebSocket消息:', message.type); - - try { - switch (message.type) { - case 'offer': - console.log('处理 offer'); - // 确保接收方的peerConnection已初始化 - if (!peerConnection) { - console.log('接收方peerConnection未初始化,先初始化'); - initPeerConnection(); - // 等待一小段时间让peerConnection完全初始化 - await new Promise(resolve => setTimeout(resolve, 100)); - } - - await peerConnection.setRemoteDescription(new RTCSessionDescription(message.payload)); - console.log('远程描述设置成功,创建 answer'); - - const answer = await peerConnection.createAnswer(); - await peerConnection.setLocalDescription(answer); - console.log('本地描述设置成功,发送 answer'); - - sendWebSocketMessage({ - type: 'answer', - payload: answer - }); - break; - - case 'answer': - console.log('处理 answer'); - if (peerConnection) { - await peerConnection.setRemoteDescription(new RTCSessionDescription(message.payload)); - console.log('远程 answer 设置成功'); - } else { - console.error('收到answer但peerConnection未初始化'); - } - break; - - case 'ice-candidate': - console.log('处理 ICE 候选:', message.payload.candidate); - if (peerConnection && peerConnection.remoteDescription) { - try { - await peerConnection.addIceCandidate(new RTCIceCandidate(message.payload)); - console.log('ICE 候选添加成功'); - } catch (error) { - console.error('添加ICE候选失败:', error); - } - } else { - console.warn('收到ICE候选但远程描述未设置,暂时缓存'); - } - break; - - case 'file-list': - if (currentRole === 'receiver') { - console.log('接收到文件列表'); - displayReceiverFiles(message.payload.files); - // 接收方在收到文件列表后初始化P2P连接 - if (!peerConnection) { - console.log('初始化接收方P2P连接'); - initPeerConnection(); - } - } - break; - - case 'receiver-ready': - if (currentRole === 'sender') { - console.log('接收方已连接,创建offer'); - // 发送方现在可以创建offer了 - setTimeout(() => { - if (peerConnection && !isP2PConnected) { - createOffer(); - } - }, 500); - } - break; - } - } catch (error) { - console.error('处理WebSocket消息失败:', error); - } -} - -// 发送WebSocket消息 -function sendWebSocketMessage(message) { - if (websocket && websocket.readyState === WebSocket.OPEN) { - websocket.send(JSON.stringify(message)); - } else { - console.warn('WebSocket未连接,无法发送消息:', message.type); - } -} diff --git a/web/static/test-websocket.html b/web/static/test-websocket.html deleted file mode 100644 index 5451b45..0000000 --- a/web/static/test-websocket.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - WebSocket连接测试 - - -

WebSocket连接测试

-
未连接
- -
- - - - diff --git a/web/static/websocket-debug.html b/web/static/websocket-debug.html deleted file mode 100644 index d965c36..0000000 --- a/web/static/websocket-debug.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - WebSocket调试测试 - - -

WebSocket连接测试

-
- - - - - -
- -
-

连接状态:未连接

-

日志:

-
-
- - - - diff --git a/web/templates/base.html b/web/templates/base.html deleted file mode 100644 index 9a84671..0000000 --- a/web/templates/base.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - {{.Title}} - 文件传输系统 - - - - - - - - - - - - - - - - - - - - - - - - - -
- {{template "content" .}} -
- - - - - - - - - {{template "scripts" .}} - - diff --git a/web/templates/index.html b/web/templates/index.html deleted file mode 100644 index e7f0b78..0000000 --- a/web/templates/index.html +++ /dev/null @@ -1,353 +0,0 @@ -{{define "content"}} - -
-
-
-

正在加载...

-
-
- -< -
-
-
-

🔗 P2P文件传输

-

安全快速的点对点文件传输

-
-
-
- -