From c717ee4d8dd7760d61b78253d839041ad6766fad Mon Sep 17 00:00:00 2001
From: stary <834207172@qq.COM>
Date: Mon, 2 Mar 2026 22:32:57 +0800
Subject: [PATCH] =?UTF-8?q?=E3=80=90m=E3=80=91=E6=8B=86=E5=88=86=E6=96=87?=
=?UTF-8?q?=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../{bidirectional => onebyone}/index1.html | 128 +---
WebApp/client/public/onebyone/script.js | 703 ++++++++++++++++++
WebApp/client/public/onebyone/style.css | 125 ++++
3 files changed, 829 insertions(+), 127 deletions(-)
rename WebApp/client/public/{bidirectional => onebyone}/index1.html (93%)
create mode 100644 WebApp/client/public/onebyone/script.js
create mode 100644 WebApp/client/public/onebyone/style.css
diff --git a/WebApp/client/public/bidirectional/index1.html b/WebApp/client/public/onebyone/index1.html
similarity index 93%
rename from WebApp/client/public/bidirectional/index1.html
rename to WebApp/client/public/onebyone/index1.html
index a82342c..d858fda 100644
--- a/WebApp/client/public/bidirectional/index1.html
+++ b/WebApp/client/public/onebyone/index1.html
@@ -6,133 +6,7 @@
VideoCall - 一对一视频通话
-
+
diff --git a/WebApp/client/public/onebyone/script.js b/WebApp/client/public/onebyone/script.js
new file mode 100644
index 0000000..38b3d6e
--- /dev/null
+++ b/WebApp/client/public/onebyone/script.js
@@ -0,0 +1,703 @@
+/**
+ * ==========================================
+ * 1. 类型定义 (Type Definitions)
+ * 后端可参考此部分设计数据库模型和 API 响应格式
+ * ==========================================
+ */
+
+/**
+ * @typedef {Object} CallSession
+ * @property {string} id - 通话唯一标识 (UUID)
+ * @property {'video'|'audio'} type - 通话类型
+ * @property {'connecting'|'ongoing'|'ended'|'failed'} status - 通话状态
+ * @property {string} startTime - ISO 8601 时间戳
+ * @property {number} duration - 已进行秒数
+ * @property {boolean} isEncrypted - 是否启用端到端加密
+ * @property {LocalUser} localUser - 本地用户信息
+ * @property {RemoteUser} remoteUser - 远端用户信息
+ */
+
+/**
+ * @typedef {Object} LocalUser
+ * @property {string} id - 用户ID
+ * @property {string} name - 显示名称
+ * @property {string} avatar - 头像URL
+ * @property {boolean} isHost - 是否主持人
+ * @property {MediaState} mediaState - 媒体状态
+ */
+
+/**
+ * @typedef {Object} RemoteUser
+ * @property {string} id - 用户ID
+ * @property {string} name - 显示名称
+ * @property {string} avatar - 头像URL
+ * @property {'online'|'offline'|'connecting'} status - 在线状态
+ * @property {MediaState} mediaState - 媒体状态
+ * @property {'excellent'|'good'|'fair'|'poor'} networkQuality - 网络质量
+ */
+
+/**
+ * @typedef {Object} MediaState
+ * @property {boolean} audio - 音频是否开启
+ * @property {boolean} video - 视频是否开启
+ * @property {boolean} screenShare - 是否屏幕共享
+ * @property {boolean} isSpeaking - 是否正在说话(VAD)
+ */
+
+/**
+ * @typedef {Object} ChatMessage
+ * @property {string} id - 消息唯一ID
+ * @property {string} senderId - 发送者ID
+ * @property {string} senderName - 发送者名称
+ * @property {string} senderAvatar - 发送者头像URL
+ * @property {string} content - 消息内容
+ * @property {'text'|'file'|'system'} type - 消息类型
+ * @property {string} timestamp - ISO 8601 时间戳
+ * @property {boolean} isSelf - 是否为自己发送
+ */
+
+/**
+ * ==========================================
+ * 2. 模拟数据层 (Mock Data Layer)
+ * 后端接口返回的数据格式应与此结构一致
+ * ==========================================
+ */
+
+// [API_RESPONSE: GET /api/call/:callId]
+const mockCallSession = {
+ id: "call-8842-2024-001",
+ type: "video",
+ status: "ongoing", // connecting | ongoing | ended | failed
+ startTime: "2024-01-15T14:30:00.000Z",
+ duration: 0, // 秒数,后端可不返回,前端本地计算
+ isEncrypted: true,
+
+ // [API_RESPONSE: 嵌套用户信息]
+ localUser: {
+ id: "user-local-001",
+ name: "我",
+ avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop",
+ isHost: true,
+ mediaState: {
+ audio: true,
+ video: true,
+ screenShare: false,
+ isSpeaking: false
+ }
+ },
+
+ remoteUser: {
+ id: "user-remote-002",
+ name: "Sarah Chen",
+ avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop",
+ status: "online", // online | offline | connecting
+ networkQuality: "excellent", // excellent | good | fair | poor
+ mediaState: {
+ audio: true,
+ video: true,
+ screenShare: false,
+ isSpeaking: false
+ }
+ }
+};
+
+// [API_RESPONSE: GET /api/call/:callId/messages]
+const mockMessages = [
+ {
+ id: "msg-001",
+ senderId: "system",
+ senderName: "系统",
+ senderAvatar: "/assets/system.png",
+ content: "通话已建立连接",
+ type: "system",
+ timestamp: "2024-01-15T14:30:00.000Z",
+ isSelf: false
+ },
+ {
+ id: "msg-002",
+ senderId: "user-remote-002",
+ senderName: "Sarah Chen",
+ senderAvatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop",
+ content: "嗨,能听到我说话吗?",
+ type: "text",
+ timestamp: "2024-01-15T14:32:15.000Z",
+ isSelf: false
+ },
+ {
+ id: "msg-003",
+ senderId: "user-local-001",
+ senderName: "我",
+ senderAvatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop",
+ content: "很清楚!你的画面也很清晰 👍",
+ type: "text",
+ timestamp: "2024-01-15T14:32:45.000Z",
+ isSelf: true
+ }
+];
+
+/**
+ * ==========================================
+ * 3. 状态管理 (State Management)
+ * 使用简单的 Observable 模式,可替换为 Redux/Vuex/Pinia
+ * ==========================================
+ */
+
+class CallStateManager {
+ constructor() {
+ // 核心状态
+ this.state = {
+ session: { ...mockCallSession },
+ messages: [...mockMessages],
+ isSidebarOpen: false,
+ unreadCount: 0,
+ localStream: null, // MediaStream 对象
+ remoteStream: null // MediaStream 对象
+ };
+
+ // 监听器数组
+ this.listeners = [];
+
+ // 初始化
+ this.init();
+ }
+
+ // 订阅状态变化
+ subscribe(callback) {
+ this.listeners.push(callback);
+ return () => {
+ this.listeners = this.listeners.filter(cb => cb !== callback);
+ };
+ }
+
+ // 通知所有监听器
+ notify(changes) {
+ this.listeners.forEach(cb => cb(this.state, changes));
+ }
+
+ // 初始化
+ init() {
+ // 启动通话时长计时器
+ this.durationInterval = setInterval(() => {
+ this.state.session.duration++;
+ this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
+ }, 1000);
+
+ // 模拟远端音频活动 (实际应由 WebRTC VAD 检测触发)
+ this.simulateRemoteActivity();
+ }
+
+ // 更新本地媒体状态
+ updateLocalMedia(mediaType, value) {
+ this.state.session.localUser.mediaState[mediaType] = value;
+ this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType, value });
+
+ // [API_CALL: POST /api/call/:callId/media]
+ // [WEBSOCKET_EMIT: media-state-changed]
+ this.emitMediaStateChange();
+ }
+
+ // 更新远端媒体状态 (由 WebSocket 消息触发)
+ updateRemoteMedia(mediaState) {
+ this.state.session.remoteUser.mediaState = {
+ ...this.state.session.remoteUser.mediaState,
+ ...mediaState
+ };
+ this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState });
+ }
+
+ // 添加消息
+ addMessage(message) {
+ this.state.messages.push(message);
+
+ // 如果侧边栏关闭且不是自己发的,增加未读
+ if (!this.state.isSidebarOpen && !message.isSelf) {
+ this.state.unreadCount++;
+ }
+
+ this.notify({ type: 'NEW_MESSAGE', message, unreadCount: this.state.unreadCount });
+ }
+
+ // 切换侧边栏
+ toggleSidebar() {
+ this.state.isSidebarOpen = !this.state.isSidebarOpen;
+ if (this.state.isSidebarOpen) {
+ this.state.unreadCount = 0;
+ }
+ this.notify({ type: 'SIDEBAR_TOGGLE', isOpen: this.state.isSidebarOpen });
+ }
+
+ // 结束通话
+ endCall() {
+ clearInterval(this.durationInterval);
+ this.state.session.status = 'ended';
+ this.notify({ type: 'CALL_ENDED' });
+
+ // [API_CALL: POST /api/call/:callId/leave]
+ // [WEBSOCKET_EMIT: leave-call]
+ }
+
+ // 模拟远端活动 (开发测试用)
+ simulateRemoteActivity() {
+ setInterval(() => {
+ if (Math.random() > 0.7) {
+ const isSpeaking = Math.random() > 0.5;
+ this.updateRemoteMedia({ isSpeaking });
+ }
+ }, 800);
+ }
+
+ // 模拟网络质量变化 (开发测试用)
+ simulateNetworkChange() {
+ const qualities = ['excellent', 'good', 'fair', 'poor'];
+ setInterval(() => {
+ if (Math.random() > 0.8) {
+ const quality = qualities[Math.floor(Math.random() * qualities.length)];
+ this.state.session.remoteUser.networkQuality = quality;
+ this.notify({ type: 'NETWORK_CHANGE', quality });
+ }
+ }, 5000);
+ }
+
+ // 发送媒体状态到服务器
+ emitMediaStateChange() {
+ const payload = {
+ userId: this.state.session.localUser.id,
+ ...this.state.session.localUser.mediaState
+ };
+ console.log('[WebSocket Emit] media-state-changed:', payload);
+ // socket.emit('media-state-changed', payload);
+ }
+
+ // Getters
+ getState() { return this.state; }
+ getLocalUser() { return this.state.session.localUser; }
+ getRemoteUser() { return this.state.session.remoteUser; }
+ getMessages() { return this.state.messages; }
+}
+
+/**
+ * ==========================================
+ * 4. UI 渲染器 (UI Renderer)
+ * 负责将状态映射到 DOM,与状态管理解耦
+ * ==========================================
+ */
+
+class UIRenderer {
+ constructor(stateManager) {
+ this.stateManager = stateManager;
+ this.unsubscribe = stateManager.subscribe(this.render.bind(this));
+
+ // 缓存 DOM 元素
+ this.elements = {
+ // 头部
+ headerTitle: document.getElementById('headerTitle'),
+ callDuration: document.getElementById('callDuration'),
+ encryptionBadge: document.getElementById('encryptionBadge'),
+ unreadBadge: document.getElementById('unreadBadge'),
+
+ // 远端视频
+ remoteVideo: document.getElementById('remoteVideo'),
+ remoteAvatar: document.getElementById('remoteAvatar'),
+ remoteName: document.getElementById('remoteName'),
+ remoteStatus: document.getElementById('remoteStatus'),
+ remoteSpeakingIndicator: document.getElementById('remoteSpeakingIndicator'),
+ remoteAudioWave: document.getElementById('remoteAudioWave'),
+ networkStatus: document.getElementById('networkStatus'),
+ networkStatusText: document.getElementById('networkStatusText'),
+ connectingOverlay: document.getElementById('connectingOverlay'),
+
+ // 本地视频
+ localVideo: document.getElementById('localVideo'),
+ localVideoPlaceholder: document.getElementById('localVideoPlaceholder'),
+ localAudioWave: document.getElementById('localAudioWave'),
+ localInitials: document.getElementById('localInitials'),
+
+ // 侧边栏
+ sidebar: document.getElementById('sidebar'),
+ chatContent: document.getElementById('chatContent'),
+ userList: document.getElementById('userList'),
+
+ // 控制按钮
+ micBtn: document.getElementById('micBtn'),
+ videoBtn: document.getElementById('videoBtn'),
+ screenBtn: document.getElementById('screenBtn'),
+ connectionQuality: document.getElementById('connectionQuality')
+ };
+ }
+
+ // 主渲染入口
+ render(state, changes) {
+ if (!changes) {
+ // 初始化全量渲染
+ this.renderHeader(state);
+ this.renderRemoteVideo(state);
+ this.renderLocalVideo(state);
+ this.renderUserList(state);
+ this.renderMessages(state);
+ this.renderControls(state);
+ return;
+ }
+
+ // 增量更新
+ switch (changes.type) {
+ case 'DURATION_UPDATE':
+ this.updateDuration(changes.duration);
+ break;
+ case 'LOCAL_MEDIA_CHANGE':
+ this.updateLocalControl(changes.mediaType, changes.value);
+ this.updateLocalVideoUI(state);
+ break;
+ case 'REMOTE_MEDIA_CHANGE':
+ this.updateRemoteVideoUI(state);
+ break;
+ case 'NEW_MESSAGE':
+ this.appendMessage(changes.message);
+ this.updateUnreadBadge(changes.unreadCount);
+ break;
+ case 'SIDEBAR_TOGGLE':
+ this.toggleSidebarUI(changes.isOpen);
+ break;
+ case 'NETWORK_CHANGE':
+ this.updateNetworkStatus(changes.quality);
+ break;
+ case 'CALL_ENDED':
+ this.showCallEnded(state.session.duration);
+ break;
+ }
+ }
+
+ // 渲染头部信息
+ renderHeader(state) {
+ const { session } = state;
+ this.elements.headerTitle.textContent = `与 ${session.remoteUser.name} 的通话`;
+ this.elements.encryptionBadge.style.display = session.isEncrypted ? 'flex' : 'none';
+ this.updateDuration(session.duration);
+ }
+
+ updateDuration(seconds) {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ this.elements.callDuration.textContent =
+ `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+ }
+
+ // 渲染远端视频区域
+ renderRemoteVideo(state) {
+ const { remoteUser } = state.session;
+ this.elements.remoteName.textContent = remoteUser.name;
+ this.elements.remoteAvatar.src = remoteUser.avatar;
+ this.elements.remoteVideo.src = remoteUser.avatar; // 实际应为视频流
+
+ this.updateRemoteVideoUI(state);
+ this.updateNetworkStatus(remoteUser.networkQuality);
+ }
+
+ updateRemoteVideoUI(state) {
+ const { remoteUser } = state.session;
+ const media = remoteUser.mediaState;
+
+ // 音频状态
+ if (media.isSpeaking && media.audio) {
+ this.elements.remoteAudioWave.classList.remove('hidden');
+ this.elements.remoteSpeakingIndicator.classList.remove('hidden');
+ } else {
+ this.elements.remoteAudioWave.classList.add('hidden');
+ this.elements.remoteSpeakingIndicator.classList.add('hidden');
+ }
+
+ // 连接状态
+ const statusText = {
+ 'online': '正在通话',
+ 'connecting': '连接中...',
+ 'offline': '已断开'
+ };
+ this.elements.remoteStatus.textContent = statusText[remoteUser.status];
+
+ // 视频占位 (实际应根据 video track 判断)
+ if (!media.video) {
+ this.elements.remoteVideo.style.opacity = '0.3';
+ } else {
+ this.elements.remoteVideo.style.opacity = '1';
+ }
+ }
+
+ // 渲染本地视频
+ renderLocalVideo(state) {
+ const { localUser } = state.session;
+ this.elements.localVideo.src = localUser.avatar; // 实际应为视频流
+ this.elements.localInitials.textContent = localUser.name.substring(0, 2);
+ this.updateLocalVideoUI(state);
+ }
+
+ updateLocalVideoUI(state) {
+ const { localUser } = state.session;
+ const media = localUser.mediaState;
+
+ // 视频开关
+ if (!media.video) {
+ this.elements.localVideoPlaceholder.classList.remove('hidden');
+ this.elements.localVideo.style.opacity = '0';
+ } else {
+ this.elements.localVideoPlaceholder.classList.add('hidden');
+ this.elements.localVideo.style.opacity = '1';
+ }
+
+ // 音频波形
+ if (media.isSpeaking && media.audio) {
+ this.elements.localAudioWave.classList.remove('hidden');
+ } else {
+ this.elements.localAudioWave.classList.add('hidden');
+ }
+ }
+
+ // 渲染用户列表
+ renderUserList(state) {
+ // 实际项目中应根据用户列表动态生成
+ // 这里使用静态HTML,仅更新状态
+ }
+
+ // 渲染消息列表
+ renderMessages(state) {
+ // 实际项目中应根据消息列表动态生成
+ // 这里使用静态HTML,仅更新新消息
+ }
+
+ // 追加新消息
+ appendMessage(message) {
+ // 实际项目中应动态生成消息元素
+ console.log('New message:', message);
+ }
+
+ // 渲染控制按钮
+ renderControls(state) {
+ const { localUser } = state.session;
+ const media = localUser.mediaState;
+
+ this.updateLocalControl('audio', media.audio);
+ this.updateLocalControl('video', media.video);
+ this.updateLocalControl('screenShare', media.screenShare);
+ }
+
+ // 更新本地控制按钮状态
+ updateLocalControl(mediaType, value) {
+ const btnMap = {
+ 'audio': this.elements.micBtn,
+ 'video': this.elements.videoBtn,
+ 'screenShare': this.elements.screenBtn
+ };
+
+ const btn = btnMap[mediaType];
+ if (btn) {
+ btn.dataset.active = !value;
+ btn.querySelector('[data-icon="default"]').classList.toggle('hidden', !value);
+ btn.querySelector('[data-icon="active"]').classList.toggle('hidden', value);
+ }
+ }
+
+ // 更新未读消息徽章
+ updateUnreadBadge(count) {
+ if (count > 0) {
+ this.elements.unreadBadge.textContent = count;
+ this.elements.unreadBadge.classList.remove('hidden');
+ } else {
+ this.elements.unreadBadge.classList.add('hidden');
+ }
+ }
+
+ // 切换侧边栏UI
+ toggleSidebarUI(isOpen) {
+ this.elements.sidebar.classList.toggle('hidden', !isOpen);
+ }
+
+ // 更新网络状态
+ updateNetworkStatus(quality) {
+ const qualityText = {
+ 'excellent': '优秀',
+ 'good': '良好',
+ 'fair': '一般',
+ 'poor': '较差'
+ };
+
+ if (quality !== 'excellent') {
+ this.elements.networkStatus.classList.remove('hidden');
+ this.elements.networkStatusText.textContent = `网络${qualityText[quality]}`;
+ } else {
+ this.elements.networkStatus.classList.add('hidden');
+ }
+
+ this.elements.connectionQuality.textContent = `连接质量: ${qualityText[quality]}`;
+ }
+
+ // 显示通话结束
+ showCallEnded(duration) {
+ const mins = Math.floor(duration / 60);
+ const secs = duration % 60;
+ alert(`通话已结束,时长: ${mins}分${secs}秒`);
+ }
+}
+
+/**
+ * ==========================================
+ * 5. 事件处理 (Event Handlers)
+ * 处理用户交互事件
+ * ==========================================
+ */
+
+// 全局状态管理器实例
+let stateManager;
+let uiRenderer;
+
+// 初始化应用
+function initApp() {
+ stateManager = new CallStateManager();
+ uiRenderer = new UIRenderer(stateManager);
+
+ // 绑定键盘事件
+ document.addEventListener('keydown', handleKeyPress);
+}
+
+// 处理键盘事件
+function handleKeyPress(e) {
+ // 空格键静音
+ if (e.code === 'Space') {
+ e.preventDefault();
+ const micBtn = document.getElementById('micBtn');
+ toggleMute(micBtn);
+ }
+
+ // Ctrl+V 切换视频
+ if (e.ctrlKey && e.key === 'v') {
+ e.preventDefault();
+ const videoBtn = document.getElementById('videoBtn');
+ toggleVideo(videoBtn);
+ }
+}
+
+// 切换静音
+function toggleMute(btn) {
+ const isActive = btn.dataset.active === 'true';
+ stateManager.updateLocalMedia('audio', !isActive);
+}
+
+// 切换视频
+function toggleVideo(btn) {
+ const isActive = btn.dataset.active === 'true';
+ stateManager.updateLocalMedia('video', !isActive);
+}
+
+// 切换屏幕共享
+function toggleScreenShare(btn) {
+ const isActive = btn.dataset.active === 'true';
+ stateManager.updateLocalMedia('screenShare', !isActive);
+}
+
+// 切换本地视频
+function toggleLocalVideo() {
+ const videoBtn = document.getElementById('videoBtn');
+ toggleVideo(videoBtn);
+}
+
+// 切换侧边栏
+function toggleSidebar() {
+ stateManager.toggleSidebar();
+}
+
+// 结束通话
+function endCall() {
+ stateManager.endCall();
+}
+
+// 发送消息
+function sendMessage() {
+ const input = document.getElementById('chatInput');
+ const content = input.value.trim();
+
+ if (content) {
+ const newMessage = {
+ id: `msg-${Date.now()}`,
+ senderId: stateManager.getLocalUser().id,
+ senderName: stateManager.getLocalUser().name,
+ senderAvatar: stateManager.getLocalUser().avatar,
+ content: content,
+ type: 'text',
+ timestamp: new Date().toISOString(),
+ isSelf: true
+ };
+
+ stateManager.addMessage(newMessage);
+ input.value = '';
+
+ // [API_CALL: POST /api/call/:callId/message]
+ // [WEBSOCKET_EMIT: send-message]
+ console.log('[WebSocket Emit] send-message:', newMessage);
+ }
+}
+
+// 处理聊天输入提交
+function handleChatSubmit(e) {
+ if (e.key === 'Enter') {
+ sendMessage();
+ }
+}
+
+// 本地视频窗口拖拽功能
+document.addEventListener('DOMContentLoaded', function() {
+ const videoContainer = document.getElementById('localVideoContainer');
+ let isDragging = false;
+ let offsetX, offsetY;
+ const edgeThreshold = 20; // 边缘吸附阈值
+
+ videoContainer.addEventListener('mousedown', function(e) {
+ // 只有在点击容器本身而不是内部按钮时才开始拖拽
+ if (e.target === videoContainer || e.target === videoContainer.querySelector('#localVideo') || e.target === videoContainer.querySelector('#localVideoPlaceholder')) {
+ isDragging = true;
+ offsetX = e.clientX - videoContainer.getBoundingClientRect().left;
+ offsetY = e.clientY - videoContainer.getBoundingClientRect().top;
+ videoContainer.style.cursor = 'grabbing';
+ videoContainer.style.zIndex = '100';
+ }
+ });
+
+ document.addEventListener('mousemove', function(e) {
+ if (!isDragging) return;
+
+ let newX = e.clientX - offsetX;
+ let newY = e.clientY - offsetY;
+
+ // 计算屏幕边界
+ const windowWidth = window.innerWidth;
+ const windowHeight = window.innerHeight;
+ const containerWidth = videoContainer.offsetWidth;
+ const containerHeight = videoContainer.offsetHeight;
+
+ // 边缘吸附逻辑
+ if (newX < edgeThreshold) {
+ newX = 0;
+ } else if (newX > windowWidth - containerWidth - edgeThreshold) {
+ newX = windowWidth - containerWidth;
+ }
+
+ if (newY < edgeThreshold) {
+ newY = 0;
+ } else if (newY > windowHeight - containerHeight - edgeThreshold) {
+ newY = windowHeight - containerHeight;
+ }
+
+ // 更新位置
+ videoContainer.style.left = newX + 'px';
+ videoContainer.style.top = newY + 'px';
+ videoContainer.style.bottom = 'auto';
+ videoContainer.style.right = 'auto';
+ });
+
+ document.addEventListener('mouseup', function() {
+ if (isDragging) {
+ isDragging = false;
+ videoContainer.style.cursor = 'move';
+ videoContainer.style.zIndex = '10';
+ }
+ });
+});
+
+// 页面加载完成后初始化
+window.addEventListener('DOMContentLoaded', initApp);
diff --git a/WebApp/client/public/onebyone/style.css b/WebApp/client/public/onebyone/style.css
new file mode 100644
index 0000000..70e0082
--- /dev/null
+++ b/WebApp/client/public/onebyone/style.css
@@ -0,0 +1,125 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+
+body {
+ font-family: 'Inter', sans-serif;
+ background: #0f172a;
+ overflow: hidden;
+}
+
+.bg-grid {
+ background-image:
+ linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px);
+ background-size: 40px 40px;
+
+}
+
+@keyframes gridMove {
+ 0% { transform: translate(0, 0); }
+ 100% { transform: translate(20px, 20px); }
+}
+
+.glass {
+ background: rgba(30, 41, 59, 0.7);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.glass-strong {
+ background: rgba(15, 23, 42, 0.9);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+}
+
+.control-btn {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.control-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
+}
+
+.end-call-pulse {
+ animation: pulse-red 2s infinite;
+}
+
+@keyframes pulse-red {
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
+ 50% { box-shadow: 0 0 0 15px rgba(239, 68, 68, 0); }
+}
+
+.chat-bubble {
+ animation: messageSlide 0.3s ease-out;
+}
+
+@keyframes messageSlide {
+ from { opacity: 0; transform: translateX(-10px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+.custom-scrollbar::-webkit-scrollbar {
+ width: 6px;
+}
+.custom-scrollbar::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.05);
+}
+.custom-scrollbar::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 3px;
+}
+
+.audio-wave {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ height: 20px;
+}
+
+.audio-wave span {
+ width: 3px;
+ background: #10b981;
+ border-radius: 2px;
+ animation: wave 1s ease-in-out infinite;
+}
+
+.audio-wave span:nth-child(1) { animation-delay: 0s; height: 40%; }
+.audio-wave span:nth-child(2) { animation-delay: 0.1s; height: 70%; }
+.audio-wave span:nth-child(3) { animation-delay: 0.2s; height: 100%; }
+.audio-wave span:nth-child(4) { animation-delay: 0.3s; height: 60%; }
+.audio-wave span:nth-child(5) { animation-delay: 0.4s; height: 30%; }
+
+@keyframes wave {
+ 0%, 100% { transform: scaleY(0.5); }
+ 50% { transform: scaleY(1); }
+}
+
+.video-fade-in {
+ animation: videoFadeIn 0.5s ease-out;
+}
+
+@keyframes videoFadeIn {
+ from { opacity: 0; transform: scale(1.05); }
+ to { opacity: 1; transform: scale(1); }
+}
+
+/* 数据绑定标记 - 开发调试时显示 */
+[data-field]::after {
+ content: attr(data-field);
+ position: absolute;
+ top: -18px;
+ right: 0;
+ background: #f59e0b;
+ color: #000;
+ font-size: 9px;
+ padding: 1px 4px;
+ border-radius: 3px;
+ opacity: 0;
+ transition: opacity 0.2s;
+ pointer-events: none;
+ z-index: 1000;
+ white-space: nowrap;
+}
+[data-field]:hover::after { opacity: 1; }
+[data-field] { position: relative; }