diff --git a/main/xiaozhi-server/test/css/test_page.css b/main/xiaozhi-server/test/css/test_page.css index 1bfc3bd7..fb51457f 100644 --- a/main/xiaozhi-server/test/css/test_page.css +++ b/main/xiaozhi-server/test/css/test_page.css @@ -1201,6 +1201,98 @@ body { z-index: 999; } +/* 滚动条样式 */ +.chat-stream::-webkit-scrollbar, +.modal-body::-webkit-scrollbar, +.mcp-tools-list::-webkit-scrollbar { + width: 6px; +} + +.chat-stream::-webkit-scrollbar-track, +.modal-body::-webkit-scrollbar-track, +.mcp-tools-list::-webkit-scrollbar-track { + background: #2f3136; + border-radius: 3px; +} + +.chat-stream::-webkit-scrollbar-thumb, +.modal-body::-webkit-scrollbar-thumb, +.mcp-tools-list::-webkit-scrollbar-thumb { + background: #40444b; + border-radius: 3px; +} + +.chat-stream::-webkit-scrollbar-thumb:hover, +.modal-body::-webkit-scrollbar-thumb:hover, +.mcp-tools-list::-webkit-scrollbar-thumb:hover { + background: #4f545c; +} + +/* ==================== 摄像头显示区域样式 ==================== */ +.camera-container { + position: fixed; + top: 20px; + left: 20px; + width: 180px; + height: 240px; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(10px); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.2); + z-index: 1000; + display: none; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + cursor: move; +} + +.camera-container.active { + display: block; +} + +.camera-container.dragging { + cursor: grabbing; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); + border-color: rgba(88, 101, 242, 0.5); +} + +@keyframes flipWithPosition { + 0% { + transform: var(--original-transform) rotateY(0deg); + } + + 100% { + transform: var(--original-transform) rotateY(180deg); + } +} + + +.camera-container.flip { + animation: flipWithPosition 0.5s ease-in-out 1 forwards; +} + +#cameraVideo { + width: 100%; + height: 100%; + object-fit: cover; + background: #1a1a1a; + pointer-events: none; + transform: scaleX(-1); +} + +.camera-switch { + display: none; + position: fixed; + bottom: 23px; + right: 20px; + z-index: 999; + color: #fff; +} + +.camera-switch.active { + display: block; +} + /* ==================== 响应式设计 ==================== */ @media (max-width: 768px) { .container { @@ -1264,6 +1356,19 @@ body { left: 10px; bottom: 80px; } + + .chat-ipt input { + font-size: 12px; + } + + .camera-container { + width: 130px; + height: 180px; + } + + .status-indicator { + font-size: 12px; + } } @media (max-width: 480px) { @@ -1343,68 +1448,4 @@ body { width: 250px; height: 80px; } -} - -/* 滚动条样式 */ -.chat-stream::-webkit-scrollbar, -.modal-body::-webkit-scrollbar, -.mcp-tools-list::-webkit-scrollbar { - width: 6px; -} - -.chat-stream::-webkit-scrollbar-track, -.modal-body::-webkit-scrollbar-track, -.mcp-tools-list::-webkit-scrollbar-track { - background: #2f3136; - border-radius: 3px; -} - -.chat-stream::-webkit-scrollbar-thumb, -.modal-body::-webkit-scrollbar-thumb, -.mcp-tools-list::-webkit-scrollbar-thumb { - background: #40444b; - border-radius: 3px; -} - -.chat-stream::-webkit-scrollbar-thumb:hover, -.modal-body::-webkit-scrollbar-thumb:hover, -.mcp-tools-list::-webkit-scrollbar-thumb:hover { - background: #4f545c; -} - -/* ==================== 摄像头显示区域样式 ==================== */ -.camera-container { - position: fixed; - top: 20px; - left: 20px; - width: 240px; - height: 180px; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(10px); - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.2); - z-index: 1000; - display: none; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - cursor: move; -} - -.camera-container.active { - display: block; -} - -.camera-container.dragging { - cursor: grabbing; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); - border-color: rgba(88, 101, 242, 0.5); -} - -#cameraVideo { - width: 100%; - height: 100%; - object-fit: cover; - background: #1a1a1a; - pointer-events: none; - transform: scaleX(-1); } \ No newline at end of file diff --git a/main/xiaozhi-server/test/js/app.js b/main/xiaozhi-server/test/js/app.js index bca7e3a7..91f60bf6 100644 --- a/main/xiaozhi-server/test/js/app.js +++ b/main/xiaozhi-server/test/js/app.js @@ -25,6 +25,7 @@ class App { this.audioPlayer = null; this.live2dManager = null; this.cameraStream = null; + this.currentFacingMode = 'user'; } // 初始化应用 @@ -127,6 +128,8 @@ class App { async initCamera() { const cameraContainer = document.getElementById('cameraContainer'); const cameraVideo = document.getElementById('cameraVideo'); + const cameraSwitch = document.getElementById('cameraSwitch'); + const dialBtn = document.getElementById('dialBtn'); if (!cameraContainer || !cameraVideo) { log('摄像头元素未找到,跳过初始化', 'warning'); @@ -188,11 +191,24 @@ class App { } log('正在请求摄像头权限...', 'info'); this.cameraStream = await navigator.mediaDevices.getUserMedia({ - video: { width: 320, height: 240, facingMode: 'user' }, + video: { width: 320, height: 240, facingMode: this.currentFacingMode }, audio: false }); cameraVideo.srcObject = this.cameraStream; + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter(device => device.kind === 'videoinput'); + if (videoDevices.length > 1) { + if (cameraSwitch) cameraSwitch.classList.add('active'); + } cameraContainer.classList.add('active'); + + // 切换时挂断情况 + const hasActive = dialBtn.classList.contains('dial-active'); + if (!hasActive) { + cameraContainer.classList.remove('active'); + cameraSwitch.classList.remove('active'); + window.stopCamera(); + } log('摄像头已启动', 'success'); return true; } catch (error) { @@ -217,6 +233,30 @@ class App { } }; + window.switchCamera = async() => { + if (window.switchCameraTimer) return; + if (this.cameraStream) { + const currentTransform = window.getComputedStyle(cameraContainer).transform; + const originalTransform = currentTransform === 'none' ? 'translate(0px, 0px)' : currentTransform; + cameraContainer.style.setProperty('--original-transform', originalTransform); + cameraContainer.classList.add('flip'); + this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user'; + window.stopCamera(); + window.startCamera(); + + window.switchCameraTimer = setTimeout(() => { + if (this.currentFacingMode === 'user') { + cameraVideo.style.transform = 'scaleX(-1)'; + } else { + cameraVideo.style.transform = 'scaleX(1)'; + } + window.switchCameraTimer = null; + cameraContainer.classList.remove('flip'); + cameraContainer.style.removeProperty('--original-transform'); + }, 500); + } + }; + window.takePhoto = (question = '描述一下看到的物品') => { return new Promise(async (resolve) => { const canvas = document.createElement('canvas'); diff --git a/main/xiaozhi-server/test/js/ui/controller.js b/main/xiaozhi-server/test/js/ui/controller.js index bc23f298..947783a3 100644 --- a/main/xiaozhi-server/test/js/ui/controller.js +++ b/main/xiaozhi-server/test/js/ui/controller.js @@ -98,6 +98,14 @@ class UIController { }); } + // Camera switch button + const cameraSwitch = document.getElementById('cameraSwitch'); + if (cameraSwitch) { + cameraSwitch.addEventListener('click', () => { + window.switchCamera(); + }) + } + // Dial button const dialBtn = document.getElementById('dialBtn'); if (dialBtn) { @@ -115,6 +123,7 @@ class UIController { if (isConnected) { wsHandler.disconnect(); this.updateDialButton(false); + if (cameraSwitch) cameraSwitch.classList.remove('active'); this.addChatMessage('Disconnected, see you next time~😊', false); } else { // Check if OTA URL is filled @@ -153,6 +162,7 @@ class UIController { if (isActive) { // 关闭摄像头 if (typeof window.stopCamera === 'function') { + if (cameraSwitch) cameraSwitch.classList.remove('active'); window.stopCamera(); } cameraContainer.classList.remove('active'); diff --git a/main/xiaozhi-server/test/test_page.html b/main/xiaozhi-server/test/test_page.html index 22d4b0f6..7f85a127 100644 --- a/main/xiaozhi-server/test/test_page.html +++ b/main/xiaozhi-server/test/test_page.html @@ -44,6 +44,15 @@