[Asm] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>宜省互传-轻松安全快速传文本、传文件 - chuan.diuta.com</title>
<style>
/* 基础变量 - 支持深色/浅色模式 */
:root {
--primary: #6366f1;
--primary-light: #a5b4fc;
--primary-dark: #4f46e5;
--secondary: #8b5cf6;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
/* 浅色主题 */
--bg-color: #f8fafc;
--surface-color: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-tertiary: #94a3b8;
--border-color: #e2e8f0;
--border-light: #f1f5f9;
--shadow-color: rgba(15, 23, 42, 0.1);
--radius-sm: 8px;
--radius: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--shadow-xs: 0 1px 3px 0 var(--shadow-color);
--shadow-sm: 0 2px 4px 0 var(--shadow-color);
--shadow: 0 4px 8px 0 var(--shadow-color);
--shadow-md: 0 6px 12px 0 var(--shadow-color);
--shadow-lg: 0 10px 20px 0 var(--shadow-color);
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--font-sans: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
}
/* 深色主题 */
[url=home.php?mod=space&uid=945662]@media[/url] (prefers-color-scheme: dark) {
:root {
--bg-color: #0f172a;
--surface-color: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--text-tertiary: #94a3b8;
--border-color: #334155;
--border-light: #1e293b;
--shadow-color: rgba(0, 0, 0, 0.3);
}
}
/* 重置样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background-color: var(--bg-color);
color: var(--text-primary);
line-height: 1.5;
font-weight: 400;
overflow-x: hidden;
transition: var(--transition-slow);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* 应用容器 */
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
@media (min-width: 769px) {
.app-container {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
}
}
/* 头部样式 */
.app-header {
padding: 24px 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 24px;
position: relative;
}
.header-content {
display: flex;
flex-direction: column;
gap: 16px;
}
@media (min-width: 769px) {
.header-content {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
.app-title {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: flex;
align-items: center;
gap: 12px;
letter-spacing: -0.025em;
}
.app-title i {
font-size: 1.8rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.app-subtitle {
color: var(--text-secondary);
font-size: 1rem;
font-weight: 400;
max-width: 600px;
}
/* 连接状态指示器 */
.connection-status {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: var(--surface-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
transition: var(--transition);
}
.connection-status:hover {
box-shadow: var(--shadow);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--danger);
position: relative;
}
.status-dot.connected {
background: var(--success);
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
}
.status-dot.connecting {
background: var(--warning);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 主内容区 */
.main-content {
flex: 1;
padding: 0 16px 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* PC端布局 */
@media (min-width: 769px) {
.main-content {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: auto auto;
grid-template-areas:
"connection chat"
"files transfers";
gap: 24px;
padding: 0 0 24px;
}
.connection-panel {
grid-area: connection;
}
.chat-panel {
grid-area: chat;
}
.file-panel {
grid-area: files;
}
.transfer-panel {
grid-area: transfers;
}
}
/* 面板通用样式 */
.panel {
background: var(--surface-color);
border-radius: var(--radius-lg);
padding: 24px;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: var(--transition-slow);
position: relative;
overflow: hidden;
}
.panel:hover {
box-shadow: var(--shadow);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.panel-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 12px;
}
.panel-title i {
color: var(--primary);
font-size: 1.1rem;
}
.panel-actions {
display: flex;
gap: 8px;
}
.panel-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 连接管理面板 */
.my-id-section {
background: linear-gradient(135deg, var(--bg-color), var(--surface-color));
border-radius: var(--radius);
padding: 20px;
margin-bottom: 24px;
border: 1px solid var(--border-color);
}
.my-id-label {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.my-id-value {
font-family: var(--font-mono);
font-size: 1rem;
font-weight: 600;
color: var(--primary);
word-break: break-all;
padding: 16px;
background: var(--bg-color);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
margin-bottom: 16px;
line-height: 1.4;
}
/* 连接表单 */
.connect-form {
margin-top: 16px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-primary);
font-size: 0.95rem;
}
.form-input {
width: 100%;
padding: 14px 16px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
font-size: 1rem;
font-family: var(--font-sans);
transition: var(--transition);
background: var(--surface-color);
color: var(--text-primary);
}
.form-input:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
/* 已连接设备列表 */
.connected-devices {
margin-top: 24px;
flex: 1;
display: flex;
flex-direction: column;
}
.devices-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
}
.devices-list {
flex: 1;
overflow-y: auto;
max-height: 300px;
background: var(--bg-color);
border-radius: var(--radius);
padding: 16px;
border: 1px solid var(--border-color);
}
.device-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: var(--surface-color);
border-radius: var(--radius-sm);
margin-bottom: 10px;
border: 1px solid var(--border-color);
transition: var(--transition);
}
.device-item:hover {
border-color: var(--primary);
box-shadow: var(--shadow-xs);
}
.device-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.device-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 0.9rem;
}
.device-details {
flex: 1;
}
.device-name {
font-weight: 600;
margin-bottom: 4px;
}
.device-id {
font-size: 0.85rem;
color: var(--text-secondary);
font-family: var(--font-mono);
}
.device-status {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
}
.status-indicator.disconnected {
background: var(--danger);
}
/* 聊天面板 */
.chat-panel {
display: flex;
flex-direction: column;
min-height: 500px;
}
@media (max-width: 768px) {
.chat-panel {
min-height: 400px;
}
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: var(--bg-color);
border-radius: var(--radius);
margin-bottom: 20px;
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
max-width: 85%;
animation: messageIn 0.3s ease;
}
@keyframes messageIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.received {
align-self: flex-start;
}
.message.sent {
align-self: flex-end;
}
.message-bubble {
padding: 14px 18px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
line-height: 1.5;
}
.message.received .message-bubble {
background: var(--surface-color);
border: 1px solid var(--border-color);
border-bottom-left-radius: 6px;
}
.message.sent .message-bubble {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border-bottom-right-radius: 6px;
}
.message-meta {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-size: 0.8rem;
opacity: 0.7;
}
.message-sender {
font-weight: 500;
}
.chat-input-area {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.chat-input {
flex: 1;
padding: 16px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
font-size: 1rem;
resize: none;
height: 60px;
font-family: var(--font-sans);
transition: var(--transition);
background: var(--surface-color);
color: var(--text-primary);
}
.chat-input:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
/* 文件传输面板 */
.file-drop-zone {
border: 2px dashed var(--border-color);
border-radius: var(--radius);
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: var(--transition);
background: var(--surface-color);
margin-bottom: 24px;
position: relative;
overflow: hidden;
}
.file-drop-zone:hover {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.05);
}
.file-drop-zone.dragover {
background: rgba(99, 102, 241, 0.1);
border-color: var(--primary);
transform: scale(1.02);
}
.file-drop-zone-icon {
font-size: 3rem;
color: var(--primary);
margin-bottom: 16px;
opacity: 0.8;
}
.file-input {
display: none;
}
.files-list {
flex: 1;
overflow-y: auto;
max-height: 300px;
}
.file-item {
background: var(--surface-color);
padding: 16px;
border-radius: var(--radius-sm);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 16px;
border: 1px solid var(--border-color);
transition: var(--transition);
}
.file-item:hover {
border-color: var(--primary);
box-shadow: var(--shadow-xs);
}
.file-icon {
font-size: 1.8rem;
color: var(--primary);
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-color);
border-radius: 12px;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 600;
margin-bottom: 6px;
word-break: break-all;
}
.file-details {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.9rem;
color: var(--text-secondary);
}
.file-progress {
width: 100%;
height: 6px;
background: var(--bg-color);
border-radius: 3px;
margin-top: 12px;
overflow: hidden;
}
.file-progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--success), var(--primary));
width: 0%;
transition: width 0.3s ease;
border-radius: 3px;
}
/* 传输列表面板 */
.transfers-list {
flex: 1;
overflow-y: auto;
max-height: 400px;
}
.transfer-item {
background: var(--surface-color);
padding: 16px;
border-radius: var(--radius-sm);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 16px;
border: 1px solid var(--border-color);
transition: var(--transition);
}
.transfer-item:hover {
border-color: var(--primary);
box-shadow: var(--shadow-xs);
}
.transfer-icon {
font-size: 1.8rem;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
}
.transfer-icon.sending {
color: var(--warning);
background: rgba(245, 158, 11, 0.1);
}
.transfer-icon.receiving {
color: var(--info);
background: rgba(59, 130, 246, 0.1);
}
.transfer-icon.completed {
color: var(--success);
background: rgba(16, 185, 129, 0.1);
}
.transfer-info {
flex: 1;
min-width: 0;
}
.transfer-name {
font-weight: 600;
margin-bottom: 6px;
word-break: break-all;
}
.transfer-details {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
font-size: 0.9rem;
color: var(--text-secondary);
}
.transfer-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.transfer-status.sending {
background: rgba(245, 158, 11, 0.1);
color: var(--warning);
}
.transfer-status.receiving {
background: rgba(59, 130, 246, 0.1);
color: var(--info);
}
.transfer-status.completed {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
}
.transfer-status.failed {
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
}
.transfer-progress {
width: 100%;
height: 6px;
background: var(--bg-color);
border-radius: 3px;
margin-top: 12px;
overflow: hidden;
}
.transfer-progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--success), var(--primary));
width: 0%;
transition: width 0.3s ease;
border-radius: 3px;
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 12px 24px;
border: none;
border-radius: var(--radius);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
text-decoration: none;
white-space: nowrap;
font-family: var(--font-sans);
position: relative;
overflow: hidden;
}
.btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%);
transform-origin: 50% 50%;
}
.btn:active::after {
animation: ripple 0.6s ease-out;
}
@keyframes ripple {
0% {
transform: scale(0, 0);
opacity: 0.5;
}
100% {
transform: scale(20, 20);
opacity: 0;
}
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border: none;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.btn-success {
background: linear-gradient(135deg, var(--success), #34d399);
color: white;
border: none;
}
.btn-success:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 12px -1px rgba(16, 185, 129, 0.3);
}
.btn-outline {
background: transparent;
color: var(--primary);
border: 1px solid var(--border-color);
}
.btn-outline:hover:not(:disabled) {
background: rgba(99, 102, 241, 0.1);
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.btn-sm {
padding: 8px 16px;
font-size: 0.9rem;
}
.btn-lg {
padding: 16px 32px;
font-size: 1.1rem;
}
.btn-block {
width: 100%;
}
.btn-icon {
padding: 10px;
border-radius: 50%;
width: 44px;
height: 44px;
}
.btn-icon-sm {
padding: 8px;
border-radius: 50%;
width: 36px;
height: 36px;
font-size: 0.9rem;
}
/* 移动端底部导航 */
.mobile-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--surface-color);
display: flex;
justify-content: space-around;
padding: 12px 0;
box-shadow: 0 -2px 20px var(--shadow-color);
z-index: 1000;
border-top: 1px solid var(--border-color);
display: none;
}
@media (max-width: 768px) {
.mobile-nav {
display: flex;
}
.main-content {
padding-bottom: 80px;
}
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
background: none;
border: none;
color: var(--text-secondary);
font-size: 0.8rem;
cursor: pointer;
transition: var(--transition);
padding: 8px 0;
flex: 1;
border-radius: 8px;
margin: 0 4px;
}
.nav-item.active {
color: var(--primary);
background: rgba(99, 102, 241, 0.1);
}
.nav-item i {
font-size: 1.4rem;
}
/* 通知系统 */
.notifications {
position: fixed;
top: 24px;
right: 24px;
z-index: 2000;
max-width: 400px;
width: 100%;
}
.notification {
background: var(--surface-color);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 12px;
box-shadow: var(--shadow-lg);
display: flex;
align-items: flex-start;
gap: 12px;
animation: slideIn 0.3s ease;
border-left: 4px solid var(--primary);
position: relative;
overflow: hidden;
transform-origin: top right;
border: 1px solid var(--border-color);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification.success {
border-left-color: var(--success);
}
.notification.error {
border-left-color: var(--danger);
}
.notification.warning {
border-left-color: var(--warning);
}
.notification.info {
border-left-color: var(--info);
}
.notification-icon {
font-size: 1.2rem;
flex-shrink: 0;
margin-top: 2px;
}
.notification-content {
flex: 1;
}
.notification-title {
font-weight: 600;
margin-bottom: 4px;
font-size: 0.95rem;
}
.notification-message {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.4;
}
.notification-close {
background: none;
border: none;
color: var(--text-tertiary);
cursor: pointer;
font-size: 1.1rem;
padding: 4px;
border-radius: 50%;
transition: var(--transition);
flex-shrink: 0;
}
.notification-close:hover {
background: var(--bg-color);
color: var(--text-primary);
}
/* 二维码弹窗模态框 - 重新设计 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 3000;
padding: 20px;
opacity: 0;
visibility: hidden;
transition: var(--transition-slow);
backdrop-filter: blur(8px);
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal-container {
background: var(--surface-color);
border-radius: var(--radius-xl);
padding: 0;
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow: hidden;
position: relative;
transform: translateY(20px) scale(0.95);
transition: transform 0.3s ease;
box-shadow: var(--shadow-xl);
border: 1px solid var(--border-color);
}
.modal-overlay.active .modal-container {
transform: translateY(0) scale(1);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 32px;
border-bottom: 1px solid var(--border-color);
}
.modal-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 1.8rem;
color: var(--text-tertiary);
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: var(--transition);
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
background: var(--bg-color);
color: var(--text-primary);
}
/* 二维码弹窗内容 */
.qr-modal-content {
display: flex;
flex-direction: column;
height: 100%;
}
.qr-tabs {
display: flex;
background: var(--bg-color);
border-bottom: 1px solid var(--border-color);
}
.qr-tab {
flex: 1;
padding: 20px;
background: none;
border: none;
border-bottom: 3px solid transparent;
font-size: 1rem;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.qr-tab:hover {
background: rgba(99, 102, 241, 0.05);
color: var(--primary);
}
.qr-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
background: rgba(99, 102, 241, 0.1);
}
.qr-tab i {
font-size: 1.5rem;
}
.qr-tab-content {
display: none;
padding: 32px;
flex: 1;
overflow-y: auto;
}
.qr-tab-content.active {
display: block;
}
/* 生成二维码选项卡 */
.qr-generate-container {
text-align: center;
}
.qr-header {
text-align: center;
margin-bottom: 24px;
}
.qr-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.qr-subtitle {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.5;
max-width: 500px;
margin: 0 auto;
}
.qr-code-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
margin: 32px 0;
}
.qr-display {
background: white;
padding: 20px;
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
display: inline-block;
}
.qr-actions {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 24px;
}
.qr-share-options {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 24px;
text-align: left;
}
.qr-share-option {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
cursor: pointer;
transition: var(--transition);
}
.qr-share-option:hover {
background: var(--bg-color);
border-color: var(--primary);
}
.qr-share-option i {
font-size: 1.2rem;
color: var(--primary);
}
.qr-share-option div {
flex: 1;
}
.qr-share-option h4 {
font-size: 0.95rem;
font-weight: 600;
margin-bottom: 4px;
color: var(--text-primary);
}
.qr-share-option p {
font-size: 0.85rem;
color: var(--text-secondary);
}
/* 扫描二维码选项卡 */
.qr-scan-container {
text-align: center;
}
.scan-preview-container {
width: 100%;
height: 300px;
background: var(--bg-color);
border-radius: var(--radius);
margin: 0 auto 24px;
position: relative;
overflow: hidden;
border: 1px solid var(--border-color);
}
.scan-preview {
width: 100%;
height: 100%;
object-fit: cover;
}
.scan-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.7);
display: flex;
align-items: center;
justify-content: center;
}
.scan-frame {
width: 200px;
height: 200px;
border: 2px solid var(--primary);
border-radius: var(--radius);
position: relative;
box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.5);
}
.scan-frame::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--primary);
border-radius: 4px;
animation: scanLine 2s infinite linear;
}
@keyframes scanLine {
0% {
top: 0;
}
100% {
top: 100%;
}
}
.scan-frame::after {
content: '请将二维码放入框内';
position: absolute;
bottom: -40px;
left: 0;
right: 0;
text-align: center;
color: white;
font-size: 0.9rem;
font-weight: 500;
}
.scan-controls {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 24px;
}
.scan-message {
margin-top: 16px;
padding: 12px 16px;
background: rgba(99, 102, 241, 0.1);
border-radius: var(--radius);
color: var(--primary);
font-size: 0.9rem;
display: none;
border: 1px solid rgba(99, 102, 241, 0.2);
}
.scan-message.active {
display: block;
animation: fadeIn 0.3s ease;
}
.manual-input-section {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid var(--border-color);
}
/* 移动端选项卡内容 */
.mobile-tabs {
display: none;
}
@media (max-width: 768px) {
.mobile-tabs {
display: block;
}
.desktop-layout {
display: none;
}
.mobile-tab-content {
display: none;
}
.mobile-tab-content.active {
display: block;
}
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-tertiary);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state h4 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-secondary);
}
.empty-state p {
margin-bottom: 8px;
line-height: 1.5;
font-size: 0.95rem;
}
/* 文件操作按钮组 */
.file-actions {
display: flex;
gap: 12px;
margin-top: 24px;
}
/* 传输统计 */
.transfer-stats {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
font-size: 0.9rem;
color: var(--text-secondary);
}
/* 响应式调整 */
@media (max-width: 768px) {
.app-header {
padding: 16px 16px 24px;
}
.app-title {
font-size: 1.5rem;
}
.main-content {
padding: 0 16px 24px;
gap: 20px;
}
.panel {
padding: 20px;
}
.panel-title {
font-size: 1.1rem;
}
.modal-container {
margin: 0 16px;
}
.qr-tab-content {
padding: 24px;
}
.scan-preview-container {
height: 250px;
}
.scan-frame {
width: 180px;
height: 180px;
}
}
/* 加载动画 */
.loader {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid var(--border-color);
border-top: 4px solid var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 工具类 */
.text-center { text-align: center; }
.mt-1 { margin-top: 4px; }
.mt-2 { margin-top: 8px; }
.mt-3 { margin-top: 12px; }
.mt-4 { margin-top: 16px; }
.mt-5 { margin-top: 20px; }
.mb-1 { margin-bottom: 4px; }
.mb-2 { margin-bottom: 8px; }
.mb-3 { margin-bottom: 12px; }
.mb-4 { margin-bottom: 16px; }
.mb-5 { margin-bottom: 20px; }
.d-flex { display: flex; }
.align-items-center { align-items: center; }
.justify-content-center { justify-content: center; }
.justify-content-between { justify-content: space-between; }
.flex-column { flex-direction: column; }
.gap-1 { gap: 4px; }
.gap-2 { gap: 8px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.gap-5 { gap: 20px; }
.w-100 { width: 100%; }
.h-100 { height: 100%; }
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="app-container">
<!-- 头部 -->
<header class="app-header">
<div class="header-content">
<div>
<h1 class="app-title">
<i class="fas fa-bolt"></i>
宜省互传
</h1>
<p class="app-subtitle">轻松安全快速传文本、传文件</p>
</div>
<div class="connection-status">
<div class="status-dot" id="status-dot"></div>
<span id="status-text">正在连接...</span>
</div>
</div>
</header>
<!-- 移动端导航 -->
<nav class="mobile-nav">
<button class="nav-item active" data-tab="connection">
<i class="fas fa-link"></i>
<span>连接</span>
</button>
<button class="nav-item" data-tab="chat">
<i class="fas fa-comments"></i>
<span>聊天</span>
</button>
<button class="nav-item" data-tab="files">
<i class="fas fa-file-import"></i>
<span>文件</span>
</button>
<button class="nav-item" data-tab="transfers">
<i class="fas fa-exchange-alt"></i>
<span>传输</span>
</button>
</nav>
<!-- 主内容区 -->
<main class="main-content">
<!-- 连接管理 (桌面版) -->
<section class="panel connection-panel desktop-layout">
<div class="panel-header">
<h2 class="panel-title">
<i class="fas fa-link"></i>
连接管理
</h2>
<div class="panel-actions">
<!-- 二维码入口按钮 -->
<button class="btn btn-icon-sm btn-outline" id="qr-modal-btn" title="二维码连接">
<i class="fas fa-qrcode"></i>
</button>
<button class="btn btn-sm btn-primary" id="copy-btn">
<i class="far fa-copy"></i> 复制ID
</button>
</div>
</div>
<div class="panel-content">
<div class="my-id-section">
<div class="my-id-label">
<i class="fas fa-id-card"></i> 我的设备ID
</div>
<div class="my-id-value" id="my-id">正在生成设备ID...</div>
</div>
<!-- 移除了connection-tools部分 -->
<div class="connect-form">
<div class="form-group">
<label class="form-label">手动连接其他设备</label>
<div class="d-flex gap-2">
<input type="text"
class="form-input"
id="peer-id-input"
placeholder="输入对方设备ID"
autocomplete="off">
<button class="btn btn-primary btn-icon" id="connect-btn" title="连接">
<i class="fas fa-plug"></i>
</button>
</div>
</div>
</div>
<div class="connected-devices">
<div class="devices-title">
<i class="fas fa-users"></i> 已连接设备 (<span id="connected-count">0</span>)
</div>
<div class="devices-list" id="devices-list">
<div class="empty-state">
<i class="fas fa-user-friends"></i>
<h4>暂无已连接的设备</h4>
<p>连接其他设备后,可以开始聊天和传输文件</p>
</div>
</div>
</div>
</div>
</section>
<!-- 聊天面板 (桌面版) -->
<section class="panel chat-panel desktop-layout">
<div class="panel-header">
<h2 class="panel-title">
<i class="fas fa-comments"></i>
实时聊天
</h2>
<div class="panel-actions">
<span class="chat-count" id="chat-count">0 条消息</span>
<button class="btn btn-sm btn-outline">
<i class="fas fa-trash"></i> 清空
</button>
</div>
</div>
<div class="panel-content">
<div class="chat-messages" id="chat-messages">
<div class="empty-state">
<i class="fas fa-comment-dots"></i>
<h4>暂无聊天消息</h4>
<p>连接设备后即可开始聊天</p>
</div>
</div>
<div class="chat-input-area">
<textarea class="chat-input"
id="chat-input"
placeholder="输入消息...按Enter发送,Shift+Enter换行"
rows="2"></textarea>
<button class="btn btn-primary btn-icon" id="send-chat-btn">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</section>
<!-- 文件传输面板 (桌面版) -->
<section class="panel file-panel desktop-layout">
<div class="panel-header">
<h2 class="panel-title">
<i class="fas fa-file-import"></i>
文件传输
</h2>
<div class="panel-actions">
<button class="btn btn-sm btn-outline">
<i class="fas fa-trash"></i> 清空
</button>
</div>
</div>
<div class="panel-content">
<div class="file-drop-zone" id="drop-zone">
<div class="file-drop-zone-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<h3>拖放文件到此处</h3>
<p>或点击选择文件,支持多选</p>
<input type="file" class="file-input" id="file-input" multiple>
<p class="qr-instruction">最大支持2GB文件</p>
</div>
<div class="files-list" id="files-list">
<div class="empty-state">
<i class="fas fa-file-import"></i>
<h4>暂无待传输文件</h4>
<p>添加文件并发送给已连接的设备</p>
</div>
</div>
<div class="file-actions">
<div class="form-group w-100 mb-0">
<select class="form-input" id="send-to-select">
<option value="all">发送给所有设备</option>
</select>
</div>
<button class="btn btn-success" id="send-files-btn" disabled>
<i class="fas fa-paper-plane"></i> 发送文件
</button>
</div>
</div>
</section>
<!-- 传输列表面板 (桌面版) -->
<section class="panel transfer-panel desktop-layout">
<div class="panel-header">
<h2 class="panel-title">
<i class="fas fa-exchange-alt"></i>
传输列表
</h2>
<div class="panel-actions">
<button class="btn btn-sm btn-outline">
<i class="fas fa-sync-alt"></i> 刷新
</button>
</div>
</div>
<div class="panel-content">
<div class="transfers-list" id="transfers-list">
<div class="empty-state">
<i class="fas fa-exchange-alt"></i>
<h4>暂无传输任务</h4>
<p>文件传输将在此显示进度</p>
</div>
</div>
<div class="transfer-stats">
<div>正在传输: <span id="active-transfers">0</span></div>
<div>已完成: <span id="completed-transfers">0</span></div>
<div>失败: <span id="failed-transfers">0</span></div>
</div>
</div>
</section>
<!-- 移动端选项卡内容 -->
<div class="mobile-tabs">
<!-- 连接选项卡 -->
<section class="panel mobile-tab-content active" id="mobile-connection">
<div class="panel-content">
<div class="my-id-section">
<div class="my-id-label">
<i class="fas fa-id-card"></i> 我的设备ID
</div>
<div class="my-id-value" id="mobile-my-id">正在生成设备ID...</div>
</div>
<!-- 移动端二维码按钮 -->
<div style="margin-bottom: 24px;">
<button class="btn btn-outline btn-block">
<i class="fas fa-qrcode"></i> 二维码连接
</button>
</div>
<div class="connect-form">
<div class="form-group">
<label class="form-label">手动连接其他设备</label>
<div class="d-flex gap-2">
<input type="text"
class="form-input"
id="mobile-peer-id-input"
placeholder="输入对方设备ID"
autocomplete="off">
<button class="btn btn-primary btn-icon" id="mobile-connect-btn" title="连接">
<i class="fas fa-plug"></i>
</button>
</div>
</div>
</div>
<div class="connected-devices">
<div class="devices-title">
<i class="fas fa-users"></i> 已连接设备 (<span id="mobile-connected-count">0</span>)
</div>
<div class="devices-list" id="mobile-devices-list">
<div class="empty-state">
<i class="fas fa-user-friends"></i>
<h4>暂无已连接的设备</h4>
<p>连接其他设备后,可以开始聊天和传输文件</p>
</div>
</div>
</div>
</div>
</section>
<!-- 聊天选项卡 -->
<section class="panel mobile-tab-content" id="mobile-chat">
<div class="panel-content" style="height: 100%; display: flex; flex-direction: column;">
<div class="chat-messages" id="mobile-chat-messages" style="flex: 1;">
<div class="empty-state">
<i class="fas fa-comment-dots"></i>
<h4>暂无聊天消息</h4>
<p>连接设备后即可开始聊天</p>
</div>
</div>
<div class="chat-input-area">
<textarea class="chat-input"
id="mobile-chat-input"
placeholder="输入消息..."
rows="2"></textarea>
<button class="btn btn-primary btn-icon" id="mobile-send-chat-btn">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</section>
<!-- 文件选项卡 -->
<section class="panel mobile-tab-content" id="mobile-files">
<div class="panel-content">
<div class="file-drop-zone" id="mobile-drop-zone">
<div class="file-drop-zone-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<h3>选择文件</h3>
<p>点击上传文件,支持多选</p>
<input type="file" class="file-input" id="mobile-file-input" multiple>
<p class="qr-instruction">最大支持2GB文件</p>
</div>
<div class="files-list" id="mobile-files-list">
<div class="empty-state">
<i class="fas fa-file-import"></i>
<h4>暂无待传输文件</h4>
<p>添加文件并发送给已连接的设备</p>
</div>
</div>
<div class="file-actions">
<div class="form-group w-100 mb-0">
<select class="form-input" id="mobile-send-to-select">
<option value="all">发送给所有设备</option>
</select>
</div>
<button class="btn btn-success btn-block" id="mobile-send-files-btn" disabled>
<i class="fas fa-paper-plane"></i> 发送文件
</button>
</div>
</div>
</section>
<!-- 传输列表选项卡 -->
<section class="panel mobile-tab-content" id="mobile-transfers">
<div class="panel-content">
<div class="transfers-list" id="mobile-transfers-list" style="flex: 1;">
<div class="empty-state">
<i class="fas fa-exchange-alt"></i>
<h4>暂无传输任务</h4>
<p>文件传输将在此显示进度</p>
</div>
</div>
<div class="transfer-stats">
<div>正在传输: <span id="mobile-active-transfers">0</span></div>
<div>已完成: <span id="mobile-completed-transfers">0</span></div>
<div>失败: <span id="mobile-failed-transfers">0</span></div>
</div>
</div>
</section>
</div>
</main>
</div>
<!-- 通知区域 -->
<div class="notifications" id="notifications"></div>
<!-- 二维码连接弹窗 -->
<div class="modal-overlay" id="qr-connect-modal">
<div class="modal-container">
<div class="modal-header">
<div class="modal-title">
<i class="fas fa-qrcode"></i> 二维码连接
</div>
<button class="modal-close">×</button>
</div>
<div class="qr-modal-content">
<div class="qr-tabs">
<button class="qr-tab active">
<i class="fas fa-qrcode"></i>
<span>生成二维码</span>
</button>
<button class="qr-tab">
<i class="fas fa-camera"></i>
<span>扫描二维码</span>
</button>
</div>
<!-- 生成二维码选项卡 -->
<div class="qr-tab-content active" id="generate-tab">
<div class="qr-generate-container">
<div class="qr-header">
<h3 class="qr-title">我的连接二维码</h3>
<p class="qr-subtitle">使用另一台设备扫描此二维码即可快速连接到您的设备</p>
</div>
<div class="qr-code-container">
<div class="qr-display" id="qrcode"></div>
<div class="my-id-value" style="margin: 0 auto; max-width: 300px;" id="qr-my-id"></div>
</div>
<div class="qr-actions">
<button class="btn btn-outline">
<i class="far fa-copy"></i> 复制设备ID
</button>
<button class="btn btn-primary">
<i class="fas fa-download"></i> 保存二维码
</button>
</div>
<div class="qr-share-options">
<div class="qr-share-option">
<i class="fas fa-share-alt"></i>
<div>
<h4>分享连接</h4>
<p>复制连接信息到剪贴板</p>
</div>
<i class="fas fa-chevron-right"></i>
</div>
<div class="qr-share-option">
<i class="fas fa-info-circle"></i>
<div>
<h4>连接说明</h4>
<p>查看二维码连接详细步骤</p>
</div>
<i class="fas fa-chevron-right"></i>
</div>
</div>
</div>
</div>
<!-- 扫描二维码选项卡 -->
<div class="qr-tab-content" id="scan-tab">
<div class="qr-scan-container">
<div class="scan-preview-container">
<video class="scan-preview" id="scan-video" autoplay playsinline></video>
<div class="scan-overlay">
<div class="scan-frame"></div>
</div>
</div>
<div class="scan-message" id="scan-message">
正在扫描二维码...
</div>
<div class="scan-controls">
<button class="btn btn-outline" id="switch-camera-btn">
<i class="fas fa-sync-alt"></i> 切换摄像头
</button>
<button class="btn btn-primary" id="stop-scan-btn">
<i class="fas fa-stop"></i> 停止扫描
</button>
</div>
<div class="manual-input-section">
<p class="qr-subtitle" style="text-align: left; margin-bottom: 12px;">或者手动输入二维码内容:</p>
<div class="d-flex gap-2">
<input type="text"
class="form-input"
id="manual-qr-input"
placeholder="粘贴二维码内容..."
style="flex: 1;">
<button class="btn btn-outline">
<i class="fas fa-plug"></i> 连接
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 引入PeerJS库 -->
<script src="https://unpkg.com/peerjs@1.4.7/dist/peerjs.min.js"></script>
<!-- 引入二维码生成库 -->
<script src="https://cdn.jsdelivr.net/npm/qrcodejs/qrcode.min.js"></script>
<!-- 引入二维码扫描库 -->
<script src="https://cdn.jsdelivr.net/npm/jsqr/dist/jsQR.min.js"></script>
<script>
// 全局变量
let peer = null;
let myPeerId = null;
let connections = new Map(); // peerId -> connection
let chatMessages = [];
let selectedFiles = [];
let transfers = new Map(); // transferId -> transfer info
let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// 二维码弹窗相关变量
let scanVideo = null;
let scanCanvas = null;
let scanContext = null;
let scanStream = null;
let scanAnimation = null;
let currentFacingMode = 'environment'; // 默认后置摄像头
let isScanning = false;
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initApp();
setupEventListeners();
});
// 初始化应用
function initApp() {
// 生成或获取设备ID
let deviceId = localStorage.getItem('deviceId');
if (!deviceId) {
deviceId = generateDeviceId();
localStorage.setItem('deviceId', deviceId);
}
// 初始化PeerJS
initializePeer(deviceId);
// 更新UI
updateMyIdDisplay();
updateStatus('正在连接服务器...', 'connecting');
// 如果是移动端,设置移动端特定逻辑
if (isMobile) {
setupMobileFeatures();
}
// 初始化传输统计
updateTransferStats();
// 初始化聊天统计
updateChatStats();
// 添加网络状态监听
window.addEventListener('online', () => {
showNotification('网络已恢复', 'success', '网络状态');
if (peer && peer.disconnected) {
setTimeout(() => {
if (peer && !peer.destroyed) {
peer.reconnect();
}
}, 1000);
}
});
window.addEventListener('offline', () => {
showNotification('网络已断开', 'error', '网络状态');
updateStatus('网络断开', 'disconnected');
});
// 页面卸载时清理资源
window.addEventListener('beforeunload', cleanupResources);
}
// 初始化PeerJS
function initializePeer(peerId) {
// 使用更稳定的WebRTC配置
const options = {
host: '0.peerjs.com',
port: 443,
path: '/',
secure: true,
config: {
'iceServers': [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:stun1.l.google.com:19302',
'stun:global.stun.twilio.com:3478'
]
},
{
urls: 'turn:openrelay.metered.ca:80',
username: 'openrelayproject',
credential: 'openrelayproject'
},
{
urls: 'turn:openrelay.metered.ca:443',
username: 'openrelayproject',
credential: 'openrelayproject'
},
{
urls: 'turn:openrelay.metered.ca:443?transport=tcp',
username: 'openrelayproject',
credential: 'openrelayproject'
}
],
'iceCandidatePoolSize': 10,
'iceTransportPolicy': 'all'
},
debug: 1
};
try {
// 如果存在旧的peer实例,先销毁
if (peer && !peer.destroyed) {
peer.destroy();
}
peer = new Peer(peerId, options);
peer.on('open', (id) => {
console.log('PeerJS连接成功,ID:', id);
myPeerId = id;
updateMyIdDisplay();
updateStatus('已连接', 'connected');
showNotification('连接成功!可以开始连接其他设备', 'success', '连接成功');
});
peer.on('connection', (conn) => {
console.log('收到连接请求:', conn.peer);
handleNewConnection(conn);
});
peer.on('error', (err) => {
console.error('PeerJS错误:', err);
let errorMsg = '连接错误';
let shouldReconnect = false;
switch (err.type) {
case 'peer-unavailable':
errorMsg = '对方设备不可用或ID错误';
break;
case 'network':
errorMsg = '网络连接错误,请检查网络';
shouldReconnect = true;
break;
case 'server-error':
errorMsg = '服务器错误,尝试重新连接';
shouldReconnect = true;
break;
case 'disconnected':
errorMsg = '连接已断开';
shouldReconnect = true;
break;
case 'browser-incompatible':
errorMsg = '浏览器不兼容WebRTC,请使用Chrome/Firefox/Safari等现代浏览器';
break;
default:
errorMsg = err.message || `连接错误: ${err.type}`;
}
updateStatus('连接错误', 'error');
showNotification(errorMsg, 'error', '连接错误');
// 需要重连的情况
if (shouldReconnect && peer && peer.disconnected) {
setTimeout(() => {
if (peer && !peer.destroyed) {
console.log('尝试重新连接...');
peer.reconnect();
}
}, 3000);
}
});
peer.on('disconnected', () => {
console.log('PeerJS连接断开');
updateStatus('连接断开', 'disconnected');
showNotification('连接断开,正在重新连接...', 'warning', '连接断开');
// 尝试重新连接
setTimeout(() => {
if (peer && !peer.destroyed) {
peer.reconnect();
}
}, 2000);
});
peer.on('close', () => {
console.log('PeerJS连接关闭');
updateStatus('连接已关闭', 'disconnected');
});
} catch (error) {
console.error('初始化PeerJS失败:', error);
showNotification('初始化连接失败: ' + error.message, 'error', '初始化失败');
updateStatus('连接失败', 'error');
}
}
// 处理新连接
function handleNewConnection(conn) {
console.log('处理新连接:', conn.peer);
// 设置序列化选项
conn.serialization = 'json';
// 设置连接事件处理
conn.on('open', () => {
console.log('连接已建立:', conn.peer);
// 添加到连接列表
connections.set(conn.peer, conn);
updateConnectedDevicesList();
// 修复数据监听
conn.on('data', (data) => {
try {
// 确保数据是对象
if (typeof data === 'string') {
data = JSON.parse(data);
}
handleIncomingData(data, conn.peer);
} catch (error) {
console.error('解析数据失败:', error);
}
});
// 连接关闭
conn.on('close', () => {
console.log('连接关闭:', conn.peer);
connections.delete(conn.peer);
updateConnectedDevicesList();
showNotification(`设备 ${conn.peer} 已断开连接`, 'warning', '连接断开');
});
// 连接错误
conn.on('error', (err) => {
console.error('连接错误:', err);
showNotification(`与 ${conn.peer} 的连接错误: ${err.message}`, 'error', '连接错误');
});
showNotification(`设备 ${conn.peer} 已连接`, 'success', '新连接');
// 发送欢迎消息
setTimeout(() => {
if (conn.open) {
const welcomeMsg = {
type: 'chat-message',
content: '连接成功!可以开始聊天和传输文件了。',
timestamp: Date.now(),
sender: myPeerId,
messageId: 'welcome_' + Date.now()
};
conn.send(JSON.stringify(welcomeMsg));
}
}, 1000);
});
// 处理连接错误
conn.on('error', (err) => {
console.error('建立连接时出错:', err);
showNotification(`连接 ${conn.peer} 失败: ${err.message}`, 'error', '连接失败');
});
}
// 连接到其他设备
function connectToPeer() {
const peerId = document.getElementById('peer-id-input').value.trim();
if (!peerId) {
showNotification('请输入设备ID', 'error', '输入错误');
return;
}
connectToPeerById(peerId);
}
function connectToPeerMobile() {
const peerId = document.getElementById('mobile-peer-id-input').value.trim();
if (!peerId) {
showNotification('请输入设备ID', 'error', '输入错误');
return;
}
connectToPeerById(peerId);
document.getElementById('mobile-peer-id-input').value = '';
}
function connectToPeerById(peerId) {
if (peerId === myPeerId) {
showNotification('不能连接到自己', 'warning', '连接错误');
return;
}
if (connections.has(peerId)) {
showNotification('已连接到该设备', 'info', '连接状态');
return;
}
showNotification(`正在连接设备 ${peerId}...`, 'info', '连接中');
try {
// 创建连接配置
const conn = peer.connect(peerId, {
reliable: true,
serialization: 'json',
metadata: {
type: 'file-transfer',
timestamp: Date.now(),
version: '1.0'
}
});
if (!conn) {
showNotification('创建连接失败', 'error', '连接失败');
return;
}
// 设置连接事件
conn.on('open', () => {
console.log('主动连接成功:', peerId);
connections.set(peerId, conn);
updateConnectedDevicesList();
// 修复数据监听
conn.on('data', (data) => {
try {
if (typeof data === 'string') {
data = JSON.parse(data);
}
handleIncomingData(data, peerId);
} catch (error) {
console.error('解析数据失败:', error);
}
});
conn.on('close', () => {
connections.delete(peerId);
updateConnectedDevicesList();
showNotification(`设备 ${peerId} 已断开连接`, 'warning', '连接断开');
});
conn.on('error', (err) => {
console.error('连接错误:', err);
showNotification(`与 ${peerId} 的连接错误: ${err.message}`, 'error', '连接错误');
});
showNotification(`成功连接设备 ${peerId}`, 'success', '连接成功');
// 清空输入框
if (!isMobile) {
document.getElementById('peer-id-input').value = '';
}
});
conn.on('error', (err) => {
console.error('连接错误:', err);
showNotification(`连接失败: ${err.message}`, 'error', '连接失败');
});
} catch (error) {
console.error('创建连接时异常:', error);
showNotification(`连接失败: ${error.message}`, 'error', '连接失败');
}
}
// 处理传入数据
function handleIncomingData(data, fromPeer) {
console.log('收到数据:', data.type, '来自:', fromPeer);
try {
switch (data.type) {
case 'chat-message':
handleChatMessage(data, fromPeer);
break;
case 'file-start':
handleFileStart(data, fromPeer);
break;
case 'file-chunk':
handleFileChunk(data, fromPeer);
break;
case 'file-end':
handleFileEnd(data, fromPeer);
break;
case 'file-error':
handleFileError(data, fromPeer);
break;
case 'ping':
// 心跳包,回复pong
const conn = connections.get(fromPeer);
if (conn && conn.open) {
conn.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
}
break;
default:
console.warn('未知数据类型:', data.type);
}
} catch (error) {
console.error('处理数据时出错:', error);
showNotification('处理数据时出错', 'error', '数据错误');
}
}
// 处理聊天消息
function handleChatMessage(data, fromPeer) {
const message = {
id: data.messageId || Date.now() + Math.random(),
content: data.content,
sender: fromPeer,
timestamp: data.timestamp || Date.now(),
type: 'received'
};
chatMessages.push(message);
updateChatDisplay();
updateChatStats();
// 如果当前不在聊天标签页,显示通知
if (isMobile && !document.querySelector('[data-tab="chat"]').classList.contains('active')) {
const shortContent = data.content.length > 30 ? data.content.substring(0, 30) + '...' : data.content;
showNotification(`新消息来自 ${fromPeer.substring(0, 8)}...: ${shortContent}`, 'info', '新消息');
}
}
// 处理文件开始传输
function handleFileStart(data, fromPeer) {
const transferId = data.transferId;
const fileId = data.fileId;
console.log('开始接收文件:', data.fileName);
// 创建传输记录
const transferInfo = {
id: transferId,
fileId: fileId,
name: data.fileName,
size: data.fileSize,
type: data.fileType,
direction: 'receiving',
peerId: fromPeer,
status: 'receiving',
progress: 0,
chunks: [],
totalChunks: data.totalChunks,
receivedChunks: 0,
startTime: Date.now(),
chunksReceived: new Array(data.totalChunks).fill(false)
};
transfers.set(transferId, transferInfo);
updateTransfersList();
updateTransferStats();
showNotification(`开始接收文件: ${data.fileName}`, 'info', '文件接收');
}
// 处理文件块
function handleFileChunk(data, fromPeer) {
const transferId = data.transferId;
const transferInfo = transfers.get(transferId);
if (!transferInfo) {
console.error('找不到传输记录:', transferId);
return;
}
// 标记块已接收
transferInfo.chunksReceived[data.chunkIndex] = true;
transferInfo.receivedChunks++;
// 更新进度
const progress = Math.round((transferInfo.receivedChunks / transferInfo.totalChunks) * 100);
transferInfo.progress = progress;
// 存储块数据
transferInfo.chunks[data.chunkIndex] = data.data;
// 更新传输列表显示
updateTransferProgress(transferId, progress);
// 如果收到所有块,组装文件
if (transferInfo.receivedChunks === transferInfo.totalChunks) {
assembleAndSaveFile(transferInfo);
}
}
// 处理文件传输结束
function handleFileEnd(data, fromPeer) {
const transferId = data.transferId;
const transferInfo = transfers.get(transferId);
if (transferInfo) {
transferInfo.status = 'completed';
transferInfo.endTime = Date.now();
transferInfo.progress = 100;
updateTransfersList();
updateTransferStats();
showNotification(`文件 ${transferInfo.name} 接收完成`, 'success', '文件接收完成');
// 发送确认
const conn = connections.get(fromPeer);
if (conn && conn.open) {
conn.send(JSON.stringify({
type: 'file-ack',
transferId: transferId,
status: 'success'
}));
}
}
}
// 处理文件传输错误
function handleFileError(data, fromPeer) {
const transferId = data.transferId;
const transferInfo = transfers.get(transferId);
if (transferInfo) {
transferInfo.status = 'failed';
transferInfo.error = data.error;
updateTransfersList();
updateTransferStats();
showNotification(`文件 ${transferInfo.name} 传输失败: ${data.error}`, 'error', '传输失败');
}
}
// 组装并保存文件
function assembleAndSaveFile(transferInfo) {
try {
console.log('开始组装文件:', transferInfo.name);
// 按顺序组装块
const sortedChunks = [];
for (let i = 0; i < transferInfo.totalChunks; i++) {
if (transferInfo.chunks[i]) {
sortedChunks.push(transferInfo.chunks[i]);
}
}
const base64Data = sortedChunks.join('');
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([bytes], { type: transferInfo.type });
const url = URL.createObjectURL(blob);
// 创建下载链接
const a = document.createElement('a');
a.href = url;
a.download = transferInfo.name;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// 清理URL对象
setTimeout(() => URL.revokeObjectURL(url), 1000);
console.log('文件保存成功:', transferInfo.name);
} catch (error) {
console.error('组装文件失败:', error);
transferInfo.status = 'failed';
transferInfo.error = error.message;
updateTransfersList();
updateTransferStats();
showNotification(`文件 ${transferInfo.name} 保存失败: ${error.message}`, 'error', '保存失败');
}
}
// 发送聊天消息
function sendChatMessage() {
const input = document.getElementById('chat-input');
sendChatMessageFromInput(input);
}
function sendChatMessageMobile() {
const input = document.getElementById('mobile-chat-input');
sendChatMessageFromInput(input);
}
function sendChatMessageFromInput(input) {
const message = input.value.trim();
if (!message) {
showNotification('消息不能为空', 'warning', '发送错误');
return;
}
if (connections.size === 0) {
showNotification('没有连接的设备', 'error', '发送错误');
return;
}
const messageData = {
type: 'chat-message',
content: message,
timestamp: Date.now(),
sender: myPeerId,
messageId: Date.now() + '_' + Math.random().toString(36).substr(2, 9)
};
// 添加到本地消息列表
const localMessage = {
id: messageData.messageId,
content: message,
sender: '我',
timestamp: Date.now(),
type: 'sent'
};
chatMessages.push(localMessage);
updateChatDisplay();
updateChatStats();
// 发送给所有连接的设备
let sentCount = 0;
const messageString = JSON.stringify(messageData);
connections.forEach((conn, peerId) => {
if (conn && conn.open) {
try {
conn.send(messageString);
sentCount++;
} catch (error) {
console.error('发送消息失败:', error);
showNotification(`发送给 ${peerId} 失败: ${error.message}`, 'error', '发送失败');
}
}
});
if (sentCount === 0) {
showNotification('消息发送失败,无可用连接', 'error', '发送失败');
// 移除刚才添加的消息
chatMessages.pop();
updateChatDisplay();
updateChatStats();
} else {
// 清空输入框
input.value = '';
input.focus();
// 滚动到最新消息
setTimeout(() => {
const chatContainer = isMobile ?
document.getElementById('mobile-chat-messages') :
document.getElementById('chat-messages');
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}, 100);
}
}
// 清空聊天记录
function clearChat() {
if (chatMessages.length === 0) {
showNotification('聊天记录已为空', 'info', '操作提示');
return;
}
if (confirm(`确定要清空 ${chatMessages.length} 条聊天记录吗?`)) {
chatMessages = [];
updateChatDisplay();
updateChatStats();
showNotification('聊天记录已清空', 'success', '操作完成');
}
}
// 发送文件
async function sendSelectedFiles() {
if (selectedFiles.length === 0) {
showNotification('请先选择文件', 'warning', '发送错误');
return;
}
if (connections.size === 0) {
showNotification('没有连接的设备', 'error', '发送错误');
return;
}
const sendTo = document.getElementById('send-to-select').value;
const recipients = sendTo === 'all' ? Array.from(connections.keys()) : [sendTo];
if (recipients.length === 0) {
showNotification('没有可发送的设备', 'error', '发送错误');
return;
}
// 发送每个文件
for (const file of selectedFiles) {
await sendFile(file, recipients);
// 每个文件之间延迟1秒,避免网络拥塞
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
async function sendSelectedFilesMobile() {
if (selectedFiles.length === 0) {
showNotification('请先选择文件', 'warning', '发送错误');
return;
}
if (connections.size === 0) {
showNotification('没有连接的设备', 'error', '发送错误');
return;
}
const sendTo = document.getElementById('mobile-send-to-select').value;
const recipients = sendTo === 'all' ? Array.from(connections.keys()) : [sendTo];
if (recipients.length === 0) {
showNotification('没有可发送的设备', 'error', '发送错误');
return;
}
// 发送每个文件
for (const file of selectedFiles) {
await sendFile(file, recipients);
// 每个文件之间延迟1秒,避免网络拥塞
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// 发送单个文件
async function sendFile(file, recipients) {
const transferId = 'transfer_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const fileId = 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
try {
// 创建传输记录
const transferInfo = {
id: transferId,
fileId: fileId,
name: file.name,
size: file.size,
type: file.type,
direction: 'sending',
recipients: recipients,
status: 'sending',
progress: 0,
startTime: Date.now()
};
transfers.set(transferId, transferInfo);
updateTransfersList();
updateTransferStats();
// 读取文件为Base64 - 减小块大小提高稳定性
const base64Data = await readFileAsBase64(file.file);
const pureBase64 = base64Data.split(',')[1];
// 减小块大小为16KB以提高稳定性
const chunkSize = 16 * 1024;
const totalChunks = Math.ceil(pureBase64.length / chunkSize);
// 发送文件信息给每个接收者
recipients.forEach(peerId => {
const conn = connections.get(peerId);
if (conn && conn.open) {
try {
conn.send(JSON.stringify({
type: 'file-start',
transferId: transferId,
fileId: fileId,
fileName: file.name,
fileSize: file.size,
fileType: file.type,
totalChunks: totalChunks,
chunkSize: chunkSize,
timestamp: Date.now()
}));
} catch (error) {
console.error('发送文件开始信息失败:', error);
}
}
});
// 分块发送文件数据,增加延迟避免拥塞
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, pureBase64.length);
const chunk = pureBase64.substring(start, end);
const chunkData = {
type: 'file-chunk',
transferId: transferId,
chunkIndex: i,
data: chunk,
totalChunks: totalChunks
};
// 发送给所有接收者
let sentToAll = true;
recipients.forEach(peerId => {
const conn = connections.get(peerId);
if (conn && conn.open) {
try {
conn.send(JSON.stringify(chunkData));
} catch (error) {
console.error('发送文件块失败:', error);
sentToAll = false;
}
} else {
sentToAll = false;
}
});
if (!sentToAll) {
throw new Error('部分设备连接丢失');
}
// 更新进度
const progress = Math.round(((i + 1) / totalChunks) * 100);
transferInfo.progress = progress;
updateTransferProgress(transferId, progress);
// 每发送10个块休息一下,避免拥塞
if (i % 10 === 0) {
await new Promise(resolve => setTimeout(resolve, 50));
}
}
// 发送完成标记
recipients.forEach(peerId => {
const conn = connections.get(peerId);
if (conn && conn.open) {
try {
conn.send(JSON.stringify({
type: 'file-end',
transferId: transferId,
success: true
}));
} catch (error) {
console.error('发送文件结束信息失败:', error);
}
}
});
// 更新传输状态
transferInfo.status = 'completed';
transferInfo.endTime = Date.now();
transferInfo.progress = 100;
updateTransfersList();
updateTransferStats();
showNotification(`文件 ${file.name} 发送完成`, 'success', '文件发送完成');
// 从选中文件列表中移除
removeFileFromList(file.id);
} catch (error) {
console.error('发送文件失败:', error);
// 更新传输状态为失败
const transferInfo = transfers.get(transferId);
if (transferInfo) {
transferInfo.status = 'failed';
transferInfo.error = error.message;
transferInfo.endTime = Date.now();
updateTransfersList();
updateTransferStats();
}
// 发送错误通知给接收者
recipients.forEach(peerId => {
const conn = connections.get(peerId);
if (conn && conn.open) {
try {
conn.send(JSON.stringify({
type: 'file-error',
transferId: transferId,
error: error.message
}));
} catch (error) {
console.error('发送错误信息失败:', error);
}
}
});
showNotification(`文件 ${file.name} 发送失败: ${error.message}`, 'error', '发送失败');
}
}
// 文件处理函数
function readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// 更新我的ID显示
function updateMyIdDisplay() {
if (myPeerId) {
document.getElementById('my-id').textContent = myPeerId;
if (isMobile) {
document.getElementById('mobile-my-id').textContent = myPeerId;
}
document.getElementById('qr-my-id').textContent = myPeerId;
}
}
// 更新状态显示
function updateStatus(text, status) {
const statusElement = document.getElementById('status-text');
const statusDot = document.getElementById('status-dot');
statusElement.textContent = text;
statusDot.className = 'status-dot';
statusDot.classList.add(status);
}
// 更新已连接设备列表
function updateConnectedDevicesList() {
const desktopList = document.getElementById('devices-list');
const mobileList = document.getElementById('mobile-devices-list');
const sendToSelect = document.getElementById('send-to-select');
const mobileSendToSelect = document.getElementById('mobile-send-to-select');
const connectedCount = connections.size;
document.getElementById('connected-count').textContent = connectedCount;
document.getElementById('mobile-connected-count').textContent = connectedCount;
if (connectedCount === 0) {
desktopList.innerHTML = `
<div class="empty-state">
<i class="fas fa-user-friends"></i>
<h4>暂无已连接的设备</h4>
<p>连接其他设备后,可以开始聊天和传输文件</p>
</div>
`;
mobileList.innerHTML = `
<div class="empty-state">
<i class="fas fa-user-friends"></i>
<h4>暂无已连接的设备</h4>
<p>连接其他设备后,可以开始聊天和传输文件</p>
</div>
`;
sendToSelect.innerHTML = '<option value="all">发送给所有设备</option>';
mobileSendToSelect.innerHTML = '<option value="all">发送给所有设备</option>';
// 禁用发送按钮
document.getElementById('send-files-btn').disabled = true;
document.getElementById('mobile-send-files-btn').disabled = true;
return;
}
// 更新设备列表
let devicesHtml = '';
let selectHtml = '<option value="all">发送给所有设备</option>';
connections.forEach((conn, peerId) => {
const shortPeerId = peerId.length > 12 ? peerId.substring(0, 12) + '...' : peerId;
const avatarText = peerId.substring(0, 2).toUpperCase();
devicesHtml += `
<div class="device-item">
<div class="device-info">
<div class="device-avatar">${avatarText}</div>
<div class="device-details">
<div class="device-name">设备 ${peerId.substring(0, 6)}...</div>
<div class="device-id">${shortPeerId}</div>
</div>
</div>
<div class="device-status">
<div class="status-indicator ${conn.open ? '' : 'disconnected'}"></div>
<span style="color: var(--text-secondary); font-size: 0.85rem;">
${conn.open ? '在线' : '离线'}
</span>
<button class="btn btn-sm btn-outline" style="margin-left: 10px;">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
if (conn.open) {
const displayName = peerId.length > 8 ? peerId.substring(0, 8) + '...' : peerId;
selectHtml += `<option value="${peerId}">发送给 ${displayName}</option>`;
}
});
desktopList.innerHTML = devicesHtml;
mobileList.innerHTML = devicesHtml;
sendToSelect.innerHTML = selectHtml;
mobileSendToSelect.innerHTML = selectHtml;
// 启用发送按钮
document.getElementById('send-files-btn').disabled = false;
document.getElementById('mobile-send-files-btn').disabled = false;
}
// 更新聊天显示
function updateChatDisplay() {
const desktopChat = document.getElementById('chat-messages');
const mobileChat = document.getElementById('mobile-chat-messages');
if (chatMessages.length === 0) {
desktopChat.innerHTML = `
<div class="empty-state">
<i class="fas fa-comment-dots"></i>
<h4>暂无聊天消息</h4>
<p>连接设备后即可开始聊天</p>
</div>
`;
mobileChat.innerHTML = `
<div class="empty-state">
<i class="fas fa-comment-dots"></i>
<h4>暂无聊天消息</h4>
<p>连接设备后即可开始聊天</p>
</div>
`;
return;
}
// 显示消息
let html = '';
chatMessages.forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
const senderDisplay = msg.type === 'sent' ? '我' : (msg.sender.length > 8 ? msg.sender.substring(0, 8) + '...' : msg.sender);
html += `
<div class="message ${msg.type}">
<div class="message-bubble">${escapeHtml(msg.content)}</div>
<div class="message-meta">
<span class="message-sender">${senderDisplay}</span>
<span class="message-time">${time}</span>
</div>
</div>
`;
});
desktopChat.innerHTML = html;
mobileChat.innerHTML = html;
// 滚动到底部
setTimeout(() => {
desktopChat.scrollTop = desktopChat.scrollHeight;
mobileChat.scrollTop = mobileChat.scrollHeight;
}, 100);
}
// 更新聊天统计
function updateChatStats() {
const chatCount = document.getElementById('chat-count');
chatCount.textContent = `${chatMessages.length} 条消息`;
}
// 更新传输列表
function updateTransfersList() {
const desktopList = document.getElementById('transfers-list');
const mobileList = document.getElementById('mobile-transfers-list');
if (transfers.size === 0) {
desktopList.innerHTML = `
<div class="empty-state">
<i class="fas fa-exchange-alt"></i>
<h4>暂无传输任务</h4>
<p>文件传输将在此显示进度</p>
</div>
`;
mobileList.innerHTML = `
<div class="empty-state">
<i class="fas fa-exchange-alt"></i>
<h4>暂无传输任务</h4>
<p>文件传输将在此显示进度</p>
</div>
`;
return;
}
// 更新传输列表
let desktopHtml = '';
let mobileHtml = '';
transfers.forEach((transfer, transferId) => {
const iconClass = transfer.direction === 'sending' ? 'sending' :
transfer.status === 'receiving' ? 'receiving' : 'completed';
const icon = transfer.direction === 'sending' ? 'fa-upload' : 'fa-download';
const directionText = transfer.direction === 'sending' ? '发送给' : '来自';
const peerDisplay = transfer.direction === 'sending' ?
(transfer.recipients && transfer.recipients.length > 0 ?
(transfer.recipients.length === 1 ? transfer.recipients[0].substring(0, 8) + '...' : '多个设备') : '未知设备') :
(transfer.peerId ? transfer.peerId.substring(0, 8) + '...' : '未知设备');
const fileSize = formatBytes(transfer.size);
const transferItem = `
<div class="transfer-item" id="transfer-${transferId}">
<div class="transfer-icon ${iconClass}">
<i class="fas ${icon}"></i>
</div>
<div class="transfer-info">
<div class="transfer-name">${transfer.name}</div>
<div class="transfer-details">
<span>${fileSize} • ${directionText} ${peerDisplay}</span>
<span class="transfer-status ${transfer.status}">${getStatusText(transfer.status)}</span>
</div>
<div class="transfer-progress">
<div class="transfer-progress-bar" id="progress-${transferId}" style="width: ${transfer.progress}%"></div>
</div>
</div>
<div class="transfer-actions">
${transfer.status === 'completed' ?
`<button class="btn btn-sm btn-outline" title="打开文件">
<i class="fas fa-folder-open"></i>
</button>` : ''}
<button class="btn btn-sm btn-outline" title="删除记录">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
desktopHtml += transferItem;
mobileHtml += transferItem;
});
desktopList.innerHTML = desktopHtml;
mobileList.innerHTML = mobileHtml;
}
// 更新传输进度
function updateTransferProgress(transferId, progress) {
const progressBar = document.getElementById(`progress-${transferId}`);
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
}
// 更新传输统计
function updateTransferStats() {
let active = 0;
let completed = 0;
let failed = 0;
transfers.forEach(transfer => {
if (transfer.status === 'sending' || transfer.status === 'receiving') {
active++;
} else if (transfer.status === 'completed') {
completed++;
} else if (transfer.status === 'failed') {
failed++;
}
});
document.getElementById('active-transfers').textContent = active;
document.getElementById('completed-transfers').textContent = completed;
document.getElementById('failed-transfers').textContent = failed;
document.getElementById('mobile-active-transfers').textContent = active;
document.getElementById('mobile-completed-transfers').textContent = completed;
document.getElementById('mobile-failed-transfers').textContent = failed;
}
// 获取状态文本
function getStatusText(status) {
const statusMap = {
'sending': '发送中',
'receiving': '接收中',
'completed': '已完成',
'failed': '失败'
};
return statusMap[status] || status;
}
// 断开连接
function disconnectPeer(peerId) {
const conn = connections.get(peerId);
if (conn) {
conn.close();
connections.delete(peerId);
updateConnectedDevicesList();
showNotification(`已断开与 ${peerId} 的连接`, 'info', '连接断开');
}
}
// 复制我的ID
function copyMyId() {
if (!myPeerId) {
showNotification('ID还未生成', 'error', '复制失败');
return;
}
navigator.clipboard.writeText(myPeerId).then(() => {
const copyBtn = document.getElementById('copy-btn');
if (copyBtn) {
const originalHtml = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fas fa-check"></i> 已复制';
copyBtn.style.background = 'linear-gradient(135deg, var(--success), #34d399)';
copyBtn.style.color = 'white';
copyBtn.style.border = 'none';
// 3秒后恢复原状
setTimeout(() => {
copyBtn.innerHTML = originalHtml;
copyBtn.style.background = '';
copyBtn.style.color = '';
copyBtn.style.border = '';
}, 3000);
}
showNotification('设备ID已复制到剪贴板', 'success', '复制成功');
}).catch(err => {
console.error('复制失败:', err);
showNotification('复制失败,请手动复制', 'error', '复制失败');
});
}
// 打开二维码连接弹窗
function openQRConnectModal() {
const modal = document.getElementById('qr-connect-modal');
modal.classList.add('active');
// 默认切换到生成二维码选项卡
switchQRTab('generate');
// 生成二维码
generateQRCode();
}
// 切换二维码选项卡
function switchQRTab(tab) {
// 更新选项卡按钮状态
document.querySelectorAll('.qr-tab').forEach(tabElement => {
tabElement.classList.remove('active');
});
document.querySelectorAll('.qr-tab-content').forEach(content => {
content.classList.remove('active');
});
// 激活选中的选项卡
document.querySelectorAll('.qr-tab').forEach(tabElement => {
if (tabElement.querySelector('span').textContent.includes(tab === 'generate' ? '生成' : '扫描')) {
tabElement.classList.add('active');
}
});
document.getElementById(`${tab}-tab`).classList.add('active');
// 如果切换到扫描选项卡,初始化扫码器
if (tab === 'scan') {
setTimeout(() => {
initQRCodeScanner();
}, 100);
} else {
// 如果切换到生成选项卡,停止扫码
stopScanning();
}
}
// 生成二维码
function generateQRCode() {
if (!myPeerId) {
showNotification('ID还未生成', 'error', '二维码生成失败');
return;
}
const qrContainer = document.getElementById('qrcode');
// 清空之前的二维码
qrContainer.innerHTML = '';
// 生成二维码数据(包含服务器信息以便连接)
const qrData = JSON.stringify({
type: 'peerjs-connect',
peerId: myPeerId,
server: '0.peerjs.com',
port: 443,
path: '/',
timestamp: Date.now(),
app: '零流量互传'
});
// 生成二维码
new QRCode(qrContainer, {
text: qrData,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}
// 复制二维码到剪贴板
function copyQRCodeToClipboard() {
const qrCanvas = document.querySelector('#qrcode canvas');
if (qrCanvas) {
qrCanvas.toBlob((blob) => {
const item = new ClipboardItem({ "image/png": blob });
navigator.clipboard.write([item]).then(() => {
showNotification('二维码已复制到剪贴板', 'success', '复制成功');
}).catch(err => {
console.error('复制二维码失败:', err);
showNotification('复制失败,请手动保存', 'error', '复制失败');
});
});
}
}
// 显示分享说明
function showShareInstructions() {
alert('连接说明:\n\n1. 确保两台设备都在线并打开此应用\n2. 在设备A上点击"生成二维码"\n3. 在设备B上点击"扫描二维码",扫描设备A的二维码\n4. 连接成功后即可开始聊天和传输文件');
}
// 下载二维码
function downloadQRCode() {
const qrCanvas = document.querySelector('#qrcode canvas');
if (qrCanvas) {
const link = document.createElement('a');
link.download = `qrcode-${myPeerId}.png`;
link.href = qrCanvas.toDataURL('image/png');
link.click();
showNotification('二维码已保存', 'success', '保存成功');
}
}
// 初始化二维码扫描器
function initQRCodeScanner() {
scanVideo = document.getElementById('scan-video');
if (!scanVideo) {
console.error('扫码组件未找到');
return;
}
// 创建Canvas用于扫描
if (!scanCanvas) {
scanCanvas = document.createElement('canvas');
scanContext = scanCanvas.getContext('2d');
}
// 请求摄像头权限
const constraints = {
video: {
facingMode: currentFacingMode,
width: { ideal: 1280 },
height: { ideal: 720 }
}
};
navigator.mediaDevices.getUserMedia(constraints)
.then((stream) => {
scanStream = stream;
scanVideo.srcObject = stream;
scanVideo.play();
// 开始扫描
startScanning();
showScanMessage('正在扫描二维码...');
})
.catch((error) => {
console.error('摄像头访问失败:', error);
showNotification('摄像头访问失败,请检查权限', 'error', '扫码失败');
showScanMessage('摄像头访问失败,请检查权限');
// 显示手动输入区域
document.getElementById('manual-qr-input').style.display = 'block';
});
}
// 开始扫描
function startScanning() {
if (!scanVideo || !scanCanvas || !scanContext) {
return;
}
// 设置Canvas尺寸与视频一致
scanCanvas.width = scanVideo.videoWidth;
scanCanvas.height = scanVideo.videoHeight;
// 扫描函数
function scan() {
if (scanVideo.readyState === scanVideo.HAVE_ENOUGH_DATA) {
// 绘制视频帧到Canvas
scanContext.drawImage(scanVideo, 0, 0, scanCanvas.width, scanCanvas.height);
// 获取图像数据
const imageData = scanContext.getImageData(0, 0, scanCanvas.width, scanCanvas.height);
// 使用jsQR解析二维码
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert',
});
// 如果找到二维码
if (code) {
console.log('找到二维码:', code.data);
handleScannedQR(code.data);
stopScanning();
}
}
// 继续扫描
scanAnimation = requestAnimationFrame(scan);
}
// 开始扫描循环
isScanning = true;
scanAnimation = requestAnimationFrame(scan);
}
// 处理扫描到的二维码
function handleScannedQR(decodedText) {
console.log('扫描到二维码:', decodedText);
showScanMessage('二维码扫描成功,正在处理...');
try {
// 尝试解析JSON
const qrData = JSON.parse(decodedText);
if (qrData.type === 'peerjs-connect' && qrData.peerId) {
// 自动连接
showScanMessage('正在连接设备...');
setTimeout(() => {
connectToPeerById(qrData.peerId);
showNotification('扫描成功,正在连接...', 'success', '扫码成功');
closeQRConnectModal();
}, 500);
} else {
showScanMessage('无效的二维码数据');
}
} catch (error) {
// 如果不是JSON,尝试直接作为Peer ID连接
if (decodedText.length >= 10 && decodedText.length <= 50) {
showScanMessage('正在连接设备...');
setTimeout(() => {
connectToPeerById(decodedText);
showNotification('扫描成功,正在连接...', 'success', '扫码成功');
closeQRConnectModal();
}, 500);
} else {
showScanMessage('无法识别的二维码格式');
}
}
}
// 显示扫描消息
function showScanMessage(message) {
const scanMessage = document.getElementById('scan-message');
scanMessage.textContent = message;
scanMessage.classList.add('active');
// 3秒后隐藏消息
setTimeout(() => {
scanMessage.classList.remove('active');
}, 3000);
}
// 切换摄像头
function switchCamera() {
currentFacingMode = currentFacingMode === 'environment' ? 'user' : 'environment';
// 停止当前流
stopScanning();
// 重新初始化扫码器
setTimeout(() => {
initQRCodeScanner();
}, 100);
}
// 停止扫描
function stopScanning() {
if (scanAnimation) {
cancelAnimationFrame(scanAnimation);
scanAnimation = null;
}
isScanning = false;
}
// 关闭二维码弹窗
function closeQRConnectModal() {
stopScanning();
if (scanStream) {
scanStream.getTracks().forEach(track => track.stop());
scanStream = null;
}
if (scanVideo) {
scanVideo.srcObject = null;
}
const modal = document.getElementById('qr-connect-modal');
modal.classList.remove('active');
}
// 从手动输入连接
function connectFromManualInput() {
const input = document.getElementById('manual-qr-input');
const text = input.value.trim();
if (!text) {
showNotification('请输入二维码内容', 'error', '输入错误');
return;
}
handleScannedQR(text);
input.value = '';
}
// 切换移动端选项卡
function switchMobileTab(tab) {
// 更新导航按钮状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-tab') === tab) {
item.classList.add('active');
}
});
// 更新选项卡内容
document.querySelectorAll('.mobile-tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`mobile-${tab}`).classList.add('active');
}
// 设置移动端特定功能
function setupMobileFeatures() {
// 为聊天输入框添加发送快捷键
const mobileChatInput = document.getElementById('mobile-chat-input');
if (mobileChatInput) {
mobileChatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendChatMessageMobile();
}
});
}
// 为桌面版聊天输入框添加发送快捷键
const desktopChatInput = document.getElementById('chat-input');
if (desktopChatInput) {
desktopChatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendChatMessage();
}
});
}
// 为文件输入框添加监听
const mobileFileInput = document.getElementById('mobile-file-input');
if (mobileFileInput) {
mobileFileInput.addEventListener('change', (e) => {
handleFileSelect(e, true);
});
}
// 拖放区域
const mobileDropZone = document.getElementById('mobile-drop-zone');
if (mobileDropZone) {
mobileDropZone.addEventListener('dragover', handleDragOver);
mobileDropZone.addEventListener('dragleave', handleDragLeave);
mobileDropZone.addEventListener('drop', (e) => {
handleDrop(e, true);
});
}
// 自动切换到连接标签页
switchMobileTab('connection');
}
// 处理文件选择
function handleFileSelect(event, isMobile = false) {
const files = Array.from(event.target.files);
if (files.length > 0) {
addFilesToTransferList(files, isMobile);
}
event.target.value = '';
}
// 处理拖放
function handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
event.currentTarget.classList.add('dragover');
}
function handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
event.currentTarget.classList.remove('dragover');
}
function handleDrop(event, isMobile = false) {
event.preventDefault();
event.stopPropagation();
event.currentTarget.classList.remove('dragover');
const files = Array.from(event.dataTransfer.files);
if (files.length > 0) {
addFilesToTransferList(files, isMobile);
}
}
// 添加文件到传输列表
function addFilesToTransferList(files, isMobile = false) {
files.forEach(file => {
const fileId = 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
selectedFiles.push({
id: fileId,
file: file,
name: file.name,
size: file.size,
type: file.type
});
addFileToUI({
id: fileId,
name: file.name,
size: file.size,
type: file.type
}, isMobile);
});
showNotification(`已添加 ${files.length} 个文件`, 'success', '文件添加');
// 启用发送按钮
if (!isMobile) {
document.getElementById('send-files-btn').disabled = false;
} else {
document.getElementById('mobile-send-files-btn').disabled = false;
}
}
// 添加文件到UI
function addFileToUI(file, isMobile = false) {
const fileList = isMobile ?
document.getElementById('mobile-files-list') :
document.getElementById('files-list');
// 移除空状态
if (fileList.querySelector('.empty-state')) {
fileList.innerHTML = '';
}
const fileElement = document.createElement('div');
fileElement.className = 'file-item';
fileElement.id = `file-${file.id}`;
const size = formatBytes(file.size);
const icon = getFileIcon(file.type);
fileElement.innerHTML = `
<div class="file-icon">
<i class="${icon}"></i>
</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-details">
<span>${size}</span>
<span>${getFileType(file.type)}</span>
</div>
<div class="file-progress">
<div class="file-progress-bar" id="progress-${file.id}" style="width: 0%"></div>
</div>
</div>
<button class="btn btn-sm btn-outline">
<i class="fas fa-times"></i>
</button>
`;
fileList.appendChild(fileElement);
}
// 获取文件图标
function getFileIcon(type) {
if (type.startsWith('image/')) return 'fas fa-file-image';
if (type.startsWith('video/')) return 'fas fa-file-video';
if (type.startsWith('audio/')) return 'fas fa-file-audio';
if (type === 'application/pdf') return 'fas fa-file-pdf';
if (type.includes('word')) return 'fas fa-file-word';
if (type.includes('excel') || type.includes('sheet')) return 'fas fa-file-excel';
if (type.includes('powerpoint') || type.includes('presentation')) return 'fas fa-file-powerpoint';
if (type.includes('zip') || type.includes('compressed')) return 'fas fa-file-archive';
if (type.includes('text/')) return 'fas fa-file-alt';
return 'fas fa-file';
}
// 获取文件类型
function getFileType(type) {
if (type.startsWith('image/')) return '图片';
if (type.startsWith('video/')) return '视频';
if (type.startsWith('audio/')) return '音频';
if (type === 'application/pdf') return 'PDF文档';
if (type.includes('word')) return 'Word文档';
if (type.includes('excel') || type.includes('sheet')) return 'Excel文档';
if (type.includes('powerpoint') || type.includes('presentation')) return 'PPT文档';
if (type.includes('zip') || type.includes('compressed')) return '压缩文件';
if (type.includes('text/')) return '文本文件';
return '文件';
}
// 从列表中移除文件
function removeFileFromList(fileId, isMobile = false) {
// 从selectedFiles中移除
const index = selectedFiles.findIndex(f => f.id === fileId);
if (index > -1) {
selectedFiles.splice(index, 1);
}
// 从UI中移除
const fileElement = document.getElementById(`file-${fileId}`);
if (fileElement) {
fileElement.remove();
}
// 如果列表为空,显示空状态
const desktopList = document.getElementById('files-list');
const mobileList = document.getElementById('mobile-files-list');
if (selectedFiles.length === 0) {
if (desktopList.children.length === 0) {
desktopList.innerHTML = `
<div class="empty-state">
<i class="fas fa-file-import"></i>
<h4>暂无待传输文件</h4>
<p>添加文件并发送给已连接的设备</p>
</div>
`;
}
if (mobileList.children.length === 0) {
mobileList.innerHTML = `
<div class="empty-state">
<i class="fas fa-file-import"></i>
<h4>暂无待传输文件</h4>
<p>添加文件并发送给已连接的设备</p>
</div>
`;
}
// 禁用发送按钮
document.getElementById('send-files-btn').disabled = true;
document.getElementById('mobile-send-files-btn').disabled = true;
}
}
// 清空文件列表
function clearFileList() {
if (selectedFiles.length === 0) {
showNotification('文件列表已为空', 'info', '操作提示');
return;
}
if (confirm(`确定要清空 ${selectedFiles.length} 个文件吗?`)) {
selectedFiles = [];
const desktopList = document.getElementById('files-list');
const mobileList = document.getElementById('mobile-files-list');
desktopList.innerHTML = `
<div class="empty-state">
<i class="fas fa-file-import"></i>
<h4>暂无待传输文件</h4>
<p>添加文件并发送给已连接的设备</p>
</div>
`;
mobileList.innerHTML = `
<div class="empty-state">
<i class="fas fa-file-import"></i>
<h4>暂无待传输文件</h4>
<p>添加文件并发送给已连接的设备</p>
</div>
`;
// 禁用发送按钮
document.getElementById('send-files-btn').disabled = true;
document.getElementById('mobile-send-files-btn').disabled = true;
showNotification('文件列表已清空', 'success', '操作完成');
}
}
// 打开传输文件
function openTransferFile(transferId) {
const transfer = transfers.get(transferId);
if (transfer && transfer.status === 'completed') {
showNotification('文件已保存在下载文件夹中', 'info', '文件位置');
}
}
// 移除传输记录
function removeTransfer(transferId) {
if (confirm('确定要删除此传输记录吗?')) {
transfers.delete(transferId);
updateTransfersList();
updateTransferStats();
showNotification('传输记录已删除', 'success', '操作完成');
}
}
// 刷新传输列表
function refreshTransfers() {
updateTransfersList();
updateTransferStats();
showNotification('传输列表已刷新', 'success', '刷新完成');
}
// 显示通知
function showNotification(message, type = 'info', title = '通知') {
const notifications = document.getElementById('notifications');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
let icon = 'fas fa-info-circle';
if (type === 'success') icon = 'fas fa-check-circle';
if (type === 'error') icon = 'fas fa-exclamation-circle';
if (type === 'warning') icon = 'fas fa-exclamation-triangle';
notification.innerHTML = `
<div class="notification-icon">
<i class="${icon}"></i>
</div>
<div class="notification-content">
<div class="notification-title">${title}</div>
<div class="notification-message">${message}</div>
</div>
<button class="notification-close">
<i class="fas fa-times"></i>
</button>
`;
notifications.appendChild(notification);
// 自动移除通知
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
// 清理资源
function cleanupResources() {
// 关闭所有连接
connections.forEach((conn, peerId) => {
if (conn && conn.open) {
conn.close();
}
});
// 销毁peer实例
if (peer && !peer.destroyed) {
peer.destroy();
}
// 停止扫码
stopScanning();
}
// 工具函数
function generateDeviceId() {
return 'YS_' + Math.random().toString(36).substr(2, 8).toUpperCase();
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 设置事件监听器
function setupEventListeners() {
// 桌面版文件输入
const fileInput = document.getElementById('file-input');
if (fileInput) {
fileInput.addEventListener('change', (e) => handleFileSelect(e, false));
}
// 桌面版拖放区域
const dropZone = document.getElementById('drop-zone');
if (dropZone) {
dropZone.addEventListener('dragover', handleDragOver);
dropZone.addEventListener('dragleave', handleDragLeave);
dropZone.addEventListener('drop', (e) => handleDrop(e, false));
}
// 桌面版连接输入框回车连接
const peerIdInput = document.getElementById('peer-id-input');
if (peerIdInput) {
peerIdInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
connectToPeer();
}
});
}
// 移动版连接输入框回车连接
const mobilePeerIdInput = document.getElementById('mobile-peer-id-input');
if (mobilePeerIdInput) {
mobilePeerIdInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
connectToPeerMobile();
}
});
}
// 手动输入二维码内容回车连接
const manualQrInput = document.getElementById('manual-qr-input');
if (manualQrInput) {
manualQrInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
connectFromManualInput();
}
});
}
// 页面可见性变化时更新状态
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
updateConnectedDevicesList();
updateTransfersList();
}
});
}
// 全局函数导出
window.copyMyId = copyMyId;
window.connectToPeer = connectToPeer;
window.connectToPeerMobile = connectToPeerMobile;
window.sendChatMessage = sendChatMessage;
window.sendChatMessageMobile = sendChatMessageMobile;
window.clearChat = clearChat;
window.sendSelectedFiles = sendSelectedFiles;
window.sendSelectedFilesMobile = sendSelectedFilesMobile;
window.clearFileList = clearFileList;
window.removeFileFromList = removeFileFromList;
window.disconnectPeer = disconnectPeer;
window.openQRConnectModal = openQRConnectModal;
window.closeQRConnectModal = closeQRConnectModal;
window.switchQRTab = switchQRTab;
window.downloadQRCode = downloadQRCode;
window.switchCamera = switchCamera;
window.stopScanning = stopScanning;
window.connectFromManualInput = connectFromManualInput;
window.switchMobileTab = switchMobileTab;
window.openTransferFile = openTransferFile;
window.removeTransfer = removeTransfer;
window.refreshTransfers = refreshTransfers;
window.copyQRCodeToClipboard = copyQRCodeToClipboard;
window.showShareInstructions = showShareInstructions;
</script>
</body>
</html>