通话计数和显示隐藏修改完成

This commit is contained in:
2026-04-10 21:54:33 +08:00
parent 18f8601d69
commit e9b7070219
2 changed files with 220 additions and 143 deletions

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -8,6 +9,7 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
<body class="h-screen w-screen flex flex-col text-white bg-grid relative"> <body class="h-screen w-screen flex flex-col text-white bg-grid relative">
<!-- <!--
============================================================ ============================================================
@@ -103,7 +105,8 @@
--> -->
<header class="glass-strong h-16 flex items-center justify-between px-6 z-50 border-b border-white/10"> <header class="glass-strong h-16 flex items-center justify-between px-6 z-50 border-b border-white/10">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg"> <div
class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<i class="fas fa-video text-white text-lg"></i> <i class="fas fa-video text-white text-lg"></i>
</div> </div>
<div> <div>
@@ -152,17 +155,16 @@
<div class="absolute inset-0 video-fade-in"> <div class="absolute inset-0 video-fade-in">
<!-- [DATA_FIELD: remoteUser.videoStream] [TYPE: MediaStream | null] [BIND: srcObject] --> <!-- [DATA_FIELD: remoteUser.videoStream] [TYPE: MediaStream | null] [BIND: srcObject] -->
<!-- [FALLBACK: remoteUser.avatar] [TYPE: string] [URL] --> <!-- [FALLBACK: remoteUser.avatar] [TYPE: string] [URL] -->
<video id="remoteVideo" <video id="remoteVideo" alt="对方视频" class="w-full h-full object-contain" autoplay
alt="对方视频" data-field="remoteUser.videoStream">
class="w-full h-full object-contain"
autoplay
data-field="remoteUser.videoStream">
</video> </video>
<!-- 远端未连接时的占位背景 --> <!-- 远端未连接时的占位背景 -->
<div id="remoteVideoPlaceholder" class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900/80 to-purple-900/80"> <div id="remoteVideoPlaceholder"
class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900/80 to-purple-900/80">
<div class="text-center"> <div class="text-center">
<div class="w-32 h-32 rounded-full bg-indigo-700/50 flex items-center justify-center mx-auto mb-4"> <div
class="w-32 h-32 rounded-full bg-indigo-700/50 flex items-center justify-center mx-auto mb-4">
<i class="fas fa-user text-4xl text-white/70"></i> <i class="fas fa-user text-4xl text-white/70"></i>
</div> </div>
<p class="text-white text-lg font-medium">等待对方连接...</p> <p class="text-white text-lg font-medium">等待对方连接...</p>
@@ -175,12 +177,13 @@
<div class="relative"> <div class="relative">
<!-- [DATA_FIELD: remoteUser.avatar] [TYPE: string] [URL] [REQUIRED] --> <!-- [DATA_FIELD: remoteUser.avatar] [TYPE: string] [URL] [REQUIRED] -->
<img id="remoteAvatar" <img id="remoteAvatar"
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop" src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop"
class="w-8 h-8 rounded-full object-cover" class="w-8 h-8 rounded-full object-cover" data-field="remoteUser.avatar">
data-field="remoteUser.avatar">
<!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true] --> <!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true] -->
<div id="remoteSpeakingIndicator" class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900 hidden" data-field="remoteUser.isSpeaking"></div> <div id="remoteSpeakingIndicator"
class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900 hidden"
data-field="remoteUser.isSpeaking"></div>
</div> </div>
<div> <div>
<!-- [DATA_FIELD: remoteUser.name] [TYPE: string] --> <!-- [DATA_FIELD: remoteUser.name] [TYPE: string] -->
@@ -190,7 +193,8 @@
<span id="remoteStatus" data-field="remoteUser.status">正在通话</span> <span id="remoteStatus" data-field="remoteUser.status">正在通话</span>
<!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true && remoteUser.mediaState.audio === true] --> <!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true && remoteUser.mediaState.audio === true] -->
<div id="remoteAudioWave" class="audio-wave w-6 hidden" data-field="remoteUser.audioActivity"> <div id="remoteAudioWave" class="audio-wave w-6 hidden"
data-field="remoteUser.audioActivity">
<span></span><span></span><span></span><span></span><span></span> <span></span><span></span><span></span><span></span><span></span>
</div> </div>
</div> </div>
@@ -199,7 +203,9 @@
<!-- 网络状态提示 --> <!-- 网络状态提示 -->
<!-- [CONDITIONAL_RENDER: remoteUser.networkQuality !== 'excellent'] --> <!-- [CONDITIONAL_RENDER: remoteUser.networkQuality !== 'excellent'] -->
<div id="networkStatus" class="absolute top-6 right-6 glass px-3 py-1.5 rounded-full flex items-center gap-2 text-xs hidden" data-field="remoteUser.networkQuality"> <div id="networkStatus"
class="absolute top-6 right-6 glass px-3 py-1.5 rounded-full flex items-center gap-2 text-xs hidden"
data-field="remoteUser.networkQuality">
<i class="fas fa-exclamation-triangle text-yellow-500"></i> <i class="fas fa-exclamation-triangle text-yellow-500"></i>
<!-- [DATA_FIELD: remoteUser.networkQuality] [TYPE: string] [TRANSFORM: quality => text] --> <!-- [DATA_FIELD: remoteUser.networkQuality] [TYPE: string] [TRANSFORM: quality => text] -->
<span class="text-gray-300" id="networkStatusText">网络不稳定</span> <span class="text-gray-300" id="networkStatusText">网络不稳定</span>
@@ -207,9 +213,12 @@
<!-- 连接中/重连提示 --> <!-- 连接中/重连提示 -->
<!-- [CONDITIONAL_RENDER: callSession.status === 'connecting'] --> <!-- [CONDITIONAL_RENDER: callSession.status === 'connecting'] -->
<div id="connectingOverlay" class="absolute inset-0 bg-black/60 flex items-center justify-center hidden" data-field="callSession.status"> <div id="connectingOverlay" class="absolute inset-0 bg-black/60 flex items-center justify-center hidden"
data-field="callSession.status">
<div class="text-center"> <div class="text-center">
<div class="w-12 h-12 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-3"></div> <div
class="w-12 h-12 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-3">
</div>
<p class="text-white font-medium">正在连接...</p> <p class="text-white font-medium">正在连接...</p>
<p class="text-sm text-gray-400 mt-1" id="connectingText">等待对方接受邀请</p> <p class="text-sm text-gray-400 mt-1" id="connectingText">等待对方接受邀请</p>
</div> </div>
@@ -224,20 +233,19 @@
子区域: 本地视频 (Local Video - Picture in Picture) 子区域: 本地视频 (Local Video - Picture in Picture)
数据源: LocalUser 数据源: LocalUser
--> -->
<div class="absolute bottom-6 right-6 w-64 h-48 rounded-2xl overflow-hidden shadow-2xl border-2 border-white/20 video-fade-in z-10"> <div
class="absolute bottom-6 right-6 w-64 h-48 rounded-2xl overflow-hidden shadow-2xl border-2 border-white/20 video-fade-in z-10">
<!-- [DATA_FIELD: localUser.videoStream] [TYPE: MediaStream | null] [BIND: srcObject] --> <!-- [DATA_FIELD: localUser.videoStream] [TYPE: MediaStream | null] [BIND: srcObject] -->
<!-- [FALLBACK: localUser.avatar] [TYPE: string] [URL] --> <!-- [FALLBACK: localUser.avatar] [TYPE: string] [URL] -->
<video id="localVideo" <video id="localVideo"
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=300&fit=crop" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=300&fit=crop" alt="本地视频"
alt="本地视频" class="w-full h-full object-cover" autoplay muted data-field="localUser.videoStream">
class="w-full h-full object-cover"
autoplay
muted
data-field="localUser.videoStream">
</video> </video>
<!-- [CONDITIONAL_RENDER: localUser.mediaState.video === false] --> <!-- [CONDITIONAL_RENDER: localUser.mediaState.video === false] -->
<div id="localVideoPlaceholder" class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900 to-purple-900 hidden" data-field="localUser.videoOff"> <div id="localVideoPlaceholder"
class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900 to-purple-900 hidden"
data-field="localUser.videoOff">
<span class="text-4xl font-bold" id="localInitials"></span> <span class="text-4xl font-bold" id="localInitials"></span>
</div> </div>
@@ -250,8 +258,10 @@
</div> </div>
<!-- 本地视频悬停控制 --> <!-- 本地视频悬停控制 -->
<div class="absolute inset-0 bg-black/40 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2"> <div
<button onclick="toggleLocalVideo()" class="w-8 h-8 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center transition-colors"> class="absolute inset-0 bg-black/40 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<button onclick="toggleLocalVideo()"
class="w-8 h-8 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center transition-colors">
<i class="fas fa-video text-xs" id="localVideoIcon"></i> <i class="fas fa-video text-xs" id="localVideoIcon"></i>
</button> </button>
</div> </div>
@@ -273,8 +283,8 @@
数据源: [localUser, remoteUser] 数据源: [localUser, remoteUser]
--> -->
<div class="p-4 border-b border-white/10"> <div class="p-4 border-b border-white/10">
<h3 class="text-sm font-medium text-gray-400 mb-3"> <h3 class="text-sm font-medium text-gray-400 mb-3" id="userCountDisplay">
通话成员 (<!-- [DATA_FIELD: userCount] [TYPE: number] [VALUE: 2] -->2) 通话成员 (1)
</h3> </h3>
<div class="space-y-2" id="userList"> <div class="space-y-2" id="userList">
<!-- [LOOP_START: users as user] --> <!-- [LOOP_START: users as user] -->
@@ -283,11 +293,11 @@
<div class="flex items-center gap-3 p-2 rounded-lg bg-white/5" data-user-id="remote"> <div class="flex items-center gap-3 p-2 rounded-lg bg-white/5" data-user-id="remote">
<div class="relative"> <div class="relative">
<!-- [DATA_FIELD: remoteUser.avatar] --> <!-- [DATA_FIELD: remoteUser.avatar] -->
<img src="" <img src="" class="w-10 h-10 rounded-full object-cover" data-field="remoteUser.avatar">
class="w-10 h-10 rounded-full object-cover"
data-field="remoteUser.avatar">
<!-- [CONDITIONAL_RENDER: remoteUser.status === 'online'] --> <!-- [CONDITIONAL_RENDER: remoteUser.status === 'online'] -->
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div> <div
class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900">
</div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<!-- [DATA_FIELD: remoteUser.name] --> <!-- [DATA_FIELD: remoteUser.name] -->
@@ -297,7 +307,8 @@
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- [CONDITIONAL_RENDER: remoteUser.mediaState.audio === false] --> <!-- [CONDITIONAL_RENDER: remoteUser.mediaState.audio === false] -->
<i class="fas fa-microphone-slash text-gray-500 text-xs hidden" data-field="remoteUser.muteIcon"></i> <i class="fas fa-microphone-slash text-gray-500 text-xs hidden"
data-field="remoteUser.muteIcon"></i>
<!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true] --> <!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true] -->
<div class="audio-wave w-6 hidden" data-field="remoteUser.speakingIndicator"> <div class="audio-wave w-6 hidden" data-field="remoteUser.speakingIndicator">
<span></span><span></span><span></span><span></span><span></span> <span></span><span></span><span></span><span></span><span></span>
@@ -308,9 +319,7 @@
<!-- 本地用户项 --> <!-- 本地用户项 -->
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5" data-user-id="local"> <div class="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5" data-user-id="local">
<!-- [DATA_FIELD: localUser.avatar] --> <!-- [DATA_FIELD: localUser.avatar] -->
<img src="" <img src="" class="w-10 h-10 rounded-full object-cover" data-field="localUser.avatar">
class="w-10 h-10 rounded-full object-cover"
data-field="localUser.avatar">
<div class="flex-1"> <div class="flex-1">
<div class="text-sm font-medium"> <div class="text-sm font-medium">
<!-- [DATA_FIELD: localUser.name] --> <!-- [DATA_FIELD: localUser.name] -->
@@ -318,7 +327,8 @@
<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span> <span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>
</div> </div>
<!-- [DATA_FIELD: localUser.mediaState] [TRANSFORM: state => statusText] --> <!-- [DATA_FIELD: localUser.mediaState] [TRANSFORM: state => statusText] -->
<div class="text-xs text-gray-500" id="localMediaStatus" data-field="localUser.mediaStatus">静音中</div> <div class="text-xs text-gray-500" id="localMediaStatus" data-field="localUser.mediaStatus">
静音中</div>
</div> </div>
<!-- [CONDITIONAL_RENDER: localUser.mediaState.audio === false] --> <!-- [CONDITIONAL_RENDER: localUser.mediaState.audio === false] -->
<i class="fas fa-microphone-slash text-gray-500 text-xs" data-field="localUser.muteIcon"></i> <i class="fas fa-microphone-slash text-gray-500 text-xs" data-field="localUser.muteIcon"></i>
@@ -347,17 +357,18 @@
<div class="flex gap-3"> <div class="flex gap-3">
<!-- [DATA_FIELD: message.senderAvatar] --> <!-- [DATA_FIELD: message.senderAvatar] -->
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop" <img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop"
class="w-8 h-8 rounded-full object-cover" class="w-8 h-8 rounded-full object-cover" data-field="message.senderAvatar">
data-field="message.senderAvatar">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-baseline gap-2 mb-1"> <div class="flex items-baseline gap-2 mb-1">
<!-- [DATA_FIELD: message.senderName] --> <!-- [DATA_FIELD: message.senderName] -->
<span class="text-sm font-medium text-indigo-400" data-field="message.senderName">Sarah Chen</span> <span class="text-sm font-medium text-indigo-400" data-field="message.senderName">Sarah
Chen</span>
<!-- [DATA_FIELD: message.timestamp] [FORMAT: HH:MM] --> <!-- [DATA_FIELD: message.timestamp] [FORMAT: HH:MM] -->
<span class="text-xs text-gray-500" data-field="message.time">14:32</span> <span class="text-xs text-gray-500" data-field="message.time">14:32</span>
</div> </div>
<!-- [DATA_FIELD: message.content] --> <!-- [DATA_FIELD: message.content] -->
<div class="glass px-3 py-2 rounded-2xl rounded-tl-none text-sm text-gray-200" data-field="message.content"> <div class="glass px-3 py-2 rounded-2xl rounded-tl-none text-sm text-gray-200"
data-field="message.content">
</div> </div>
</div> </div>
</div> </div>
@@ -369,14 +380,14 @@
<div class="flex gap-3 flex-row-reverse"> <div class="flex gap-3 flex-row-reverse">
<!-- [DATA_FIELD: message.senderAvatar] --> <!-- [DATA_FIELD: message.senderAvatar] -->
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop" <img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop"
class="w-8 h-8 rounded-full object-cover" class="w-8 h-8 rounded-full object-cover" data-field="message.senderAvatar">
data-field="message.senderAvatar">
<div class="flex-1 flex flex-col items-end"> <div class="flex-1 flex flex-col items-end">
<div class="flex items-baseline gap-2 mb-1 flex-row-reverse"> <div class="flex items-baseline gap-2 mb-1 flex-row-reverse">
<span class="text-sm font-medium text-green-400"></span> <span class="text-sm font-medium text-green-400"></span>
<span class="text-xs text-gray-500" data-field="message.time">14:32</span> <span class="text-xs text-gray-500" data-field="message.time">14:32</span>
</div> </div>
<div class="bg-indigo-600 px-3 py-2 rounded-2xl rounded-tr-none text-sm text-white" data-field="message.content"> <div class="bg-indigo-600 px-3 py-2 rounded-2xl rounded-tr-none text-sm text-white"
data-field="message.content">
</div> </div>
</div> </div>
</div> </div>
@@ -391,20 +402,21 @@
--> -->
<div class="p-4 border-t border-white/10"> <div class="p-4 border-t border-white/10">
<div class="glass rounded-2xl flex items-center gap-2 p-2"> <div class="glass rounded-2xl flex items-center gap-2 p-2">
<button class="w-8 h-8 rounded-full hover:bg-white/10 flex items-center justify-center text-gray-400 transition-colors" onclick="openImagePicker()"> <button
class="w-8 h-8 rounded-full hover:bg-white/10 flex items-center justify-center text-gray-400 transition-colors"
onclick="openImagePicker()">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
<!-- 隐藏的文件输入元素 --> <!-- 隐藏的文件输入元素 -->
<input type="file" id="imageInput" accept="image/*" class="hidden" onchange="handleImageUpload(event)"> <input type="file" id="imageInput" accept="image/*" class="hidden"
onchange="handleImageUpload(event)">
<!-- [INPUT_FIELD] [BIND: inputValue] [EVENT: onEnter => sendMessage()] --> <!-- [INPUT_FIELD] [BIND: inputValue] [EVENT: onEnter => sendMessage()] -->
<input type="text" <input type="text" id="chatInput" placeholder="输入消息..."
id="chatInput" class="flex-1 bg-transparent border-none outline-none text-sm text-white placeholder-gray-500 px-2"
placeholder="输入消息..." data-field="chatInput" onkeypress="handleChatSubmit(event)">
class="flex-1 bg-transparent border-none outline-none text-sm text-white placeholder-gray-500 px-2"
data-field="chatInput"
onkeypress="handleChatSubmit(event)">
<!-- [BUTTON] [EVENT: onclick => sendMessage()] --> <!-- [BUTTON] [EVENT: onclick => sendMessage()] -->
<button onclick="sendMessage()" class="w-8 h-8 rounded-full bg-indigo-600 hover:bg-indigo-700 flex items-center justify-center transition-colors"> <button onclick="sendMessage()"
class="w-8 h-8 rounded-full bg-indigo-600 hover:bg-indigo-700 flex items-center justify-center transition-colors">
<i class="fas fa-paper-plane text-xs"></i> <i class="fas fa-paper-plane text-xs"></i>
</button> </button>
</div> </div>
@@ -439,60 +451,61 @@
<!-- 麦克风控制 --> <!-- 麦克风控制 -->
<!-- [DATA_FIELD: localUser.mediaState.audio] [TYPE: boolean] --> <!-- [DATA_FIELD: localUser.mediaState.audio] [TYPE: boolean] -->
<button class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group" <button
onclick="toggleMute(this)" class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group"
id="micBtn" onclick="toggleMute(this)" id="micBtn" data-field="localUser.audio" data-active="false">
data-field="localUser.audio"
data-active="false">
<i class="fas fa-microphone text-lg" data-icon="default"></i> <i class="fas fa-microphone text-lg" data-icon="default"></i>
<i class="fas fa-microphone-slash text-lg hidden text-red-400" data-icon="active"></i> <i class="fas fa-microphone-slash text-lg hidden text-red-400" data-icon="active"></i>
<span class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity"> <span
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
静音 (Space) 静音 (Space)
</span> </span>
</button> </button>
<!-- 摄像头控制 --> <!-- 摄像头控制 -->
<!-- [DATA_FIELD: localUser.mediaState.video] [TYPE: boolean] --> <!-- [DATA_FIELD: localUser.mediaState.video] [TYPE: boolean] -->
<button class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group" <button
onclick="toggleVideo(this)" class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group"
id="videoBtn" onclick="toggleVideo(this)" id="videoBtn" data-field="localUser.video" data-active="false">
data-field="localUser.video"
data-active="false">
<i class="fas fa-video text-lg" data-icon="default"></i> <i class="fas fa-video text-lg" data-icon="default"></i>
<i class="fas fa-video-slash text-lg hidden text-red-400" data-icon="active"></i> <i class="fas fa-video-slash text-lg hidden text-red-400" data-icon="active"></i>
<span class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity"> <span
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
关闭视频 (Ctrl+V) 关闭视频 (Ctrl+V)
</span> </span>
</button> </button>
<!-- 录屏控制 --> <!-- 录屏控制 -->
<!-- [DATA_FIELD: localUser.mediaState.recording] [TYPE: boolean] --> <!-- [DATA_FIELD: localUser.mediaState.recording] [TYPE: boolean] -->
<button class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group" <button
onclick="toggleRecording(this)" class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group"
id="recordBtn" onclick="toggleRecording(this)" id="recordBtn" data-field="localUser.recording" data-active="false">
data-field="localUser.recording"
data-active="false">
<i class="fas fa-circle text-lg" data-icon="default"></i> <i class="fas fa-circle text-lg" data-icon="default"></i>
<i class="fas fa-stop text-lg hidden text-red-400" data-icon="active"></i> <i class="fas fa-stop text-lg hidden text-red-400" data-icon="active"></i>
<span class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity"> <span
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
录制 录制
</span> </span>
</button> </button>
<!-- 更多选项 --> <!-- 更多选项 -->
<button class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group"> <button
class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group">
<i class="fas fa-ellipsis-h text-lg"></i> <i class="fas fa-ellipsis-h text-lg"></i>
<span class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity"> <span
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
更多选项 更多选项
</span> </span>
</button> </button>
<!-- 结束通话 --> <!-- 结束通话 -->
<!-- [EVENT: onclick => endCall()] [API: POST /api/call/:callId/leave] --> <!-- [EVENT: onclick => endCall()] [API: POST /api/call/:callId/leave] -->
<button class="control-btn w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white end-call-pulse ml-4 relative group" <button
onclick="endCall()"> class="control-btn w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white end-call-pulse ml-4 relative group"
onclick="endCall()">
<i class="fas fa-phone-slash text-xl"></i> <i class="fas fa-phone-slash text-xl"></i>
<span class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity"> <span
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
结束通话 结束通话
</span> </span>
</button> </button>
@@ -500,17 +513,21 @@
<!-- 右侧聊天按钮 --> <!-- 右侧聊天按钮 -->
<div class="absolute right-6 flex items-center gap-3"> <div class="absolute right-6 flex items-center gap-3">
<button class="control-btn w-10 h-10 rounded-full glass flex items-center justify-center text-gray-300 hover:text-white hover:bg-white/10 transition-colors relative" onclick="toggleSidebar()"> <button
class="control-btn w-10 h-10 rounded-full glass flex items-center justify-center text-gray-300 hover:text-white hover:bg-white/10 transition-colors relative"
onclick="toggleSidebar()">
<i class="fas fa-comment-alt"></i> <i class="fas fa-comment-alt"></i>
<!-- 未读消息计数角标 --> <!-- 未读消息计数角标 -->
<span id="unreadBadge" class="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center text-xs font-bold text-white hidden">0</span> <span id="unreadBadge"
class="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center text-xs font-bold text-white hidden">0</span>
</button> </button>
</div> </div>
</footer> </footer>
<!-- 通知组件 --> <!-- 通知组件 -->
<div id="notification" class="fixed top-20 left-1/2 transform -translate-x-1/2 glass px-6 py-3 rounded-full flex items-center gap-3 opacity-0 pointer-events-none transition-all duration-300 z-50 translate-y-[-20px]"> <div id="notification"
class="fixed top-20 left-1/2 transform -translate-x-1/2 glass px-6 py-3 rounded-full flex items-center gap-3 opacity-0 pointer-events-none transition-all duration-300 z-50 translate-y-[-20px]">
<i class="fas fa-info-circle text-indigo-400"></i> <i class="fas fa-info-circle text-indigo-400"></i>
<span class="text-sm" id="notificationText">通知内容</span> <span class="text-sm" id="notificationText">通知内容</span>
</div> </div>
@@ -529,7 +546,8 @@
<button id="cancelEndCall" class="flex-1 py-2 rounded-lg glass hover:bg-white/10 transition-colors"> <button id="cancelEndCall" class="flex-1 py-2 rounded-lg glass hover:bg-white/10 transition-colors">
取消 取消
</button> </button>
<button id="confirmEndCall" class="flex-1 py-2 rounded-lg bg-red-500 hover:bg-red-600 transition-colors"> <button id="confirmEndCall"
class="flex-1 py-2 rounded-lg bg-red-500 hover:bg-red-600 transition-colors">
结束通话 结束通话
</button> </button>
</div> </div>
@@ -546,7 +564,9 @@
<h3 class="text-xl font-bold mb-2" id="callRequestName">Sarah Chen</h3> <h3 class="text-xl font-bold mb-2" id="callRequestName">Sarah Chen</h3>
<p class="text-gray-400 text-sm" id="callRequestText">正在请求与您进行视频通话</p> <p class="text-gray-400 text-sm" id="callRequestText">正在请求与您进行视频通话</p>
<div class="mt-4 flex items-center justify-center gap-4"> <div class="mt-4 flex items-center justify-center gap-4">
<img id="callRequestAvatar" src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop" class="w-16 h-16 rounded-full object-cover border-4 border-indigo-500"> <img id="callRequestAvatar"
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop"
class="w-16 h-16 rounded-full object-cover border-4 border-indigo-500">
</div> </div>
</div> </div>
<div class="flex gap-3"> <div class="flex gap-3">
@@ -556,7 +576,8 @@
<span>拒绝</span> <span>拒绝</span>
</div> </div>
</button> </button>
<button id="acceptCall" class="flex-1 py-2 rounded-lg bg-green-500 hover:bg-green-600 transition-colors"> <button id="acceptCall"
class="flex-1 py-2 rounded-lg bg-green-500 hover:bg-green-600 transition-colors">
<div class="flex items-center justify-center gap-2"> <div class="flex items-center justify-center gap-2">
<i class="fas fa-phone"></i> <i class="fas fa-phone"></i>
<span>接受</span> <span>接受</span>
@@ -571,4 +592,5 @@
</body> </body>
</html> </html>

View File

@@ -31,6 +31,7 @@ class UIRenderer {
remoteStatus: document.getElementById('remoteStatus'), remoteStatus: document.getElementById('remoteStatus'),
remoteSpeakingIndicator: document.getElementById('remoteSpeakingIndicator'), remoteSpeakingIndicator: document.getElementById('remoteSpeakingIndicator'),
remoteAudioWave: document.getElementById('remoteAudioWave'), remoteAudioWave: document.getElementById('remoteAudioWave'),
remoteInfoOverlay: document.querySelector('.absolute.top-6.left-6.glass'),
networkStatus: document.getElementById('networkStatus'), networkStatus: document.getElementById('networkStatus'),
networkStatusText: document.getElementById('networkStatusText'), networkStatusText: document.getElementById('networkStatusText'),
connectingOverlay: document.getElementById('connectingOverlay'), connectingOverlay: document.getElementById('connectingOverlay'),
@@ -47,6 +48,7 @@ class UIRenderer {
userList: document.getElementById('userList'), userList: document.getElementById('userList'),
localMediaStatus: document.getElementById('localMediaStatus'), localMediaStatus: document.getElementById('localMediaStatus'),
localMuteIcon: document.querySelector('[data-field="localUser.muteIcon"]'), localMuteIcon: document.querySelector('[data-field="localUser.muteIcon"]'),
userCountDisplay: document.getElementById('userCountDisplay'),
// 控制按钮 // 控制按钮
micBtn: document.getElementById('micBtn'), micBtn: document.getElementById('micBtn'),
videoBtn: document.getElementById('videoBtn'), videoBtn: document.getElementById('videoBtn'),
@@ -174,13 +176,26 @@ class UIRenderer {
// 渲染头部 // 渲染头部
renderHeader(session) { renderHeader(session) {
if (this.elements.headerTitle) { if (this.elements.headerTitle) {
this.elements.headerTitle.textContent = `${session.remoteUser.name} 的通话`; // 未连接时不显示红框部分
if (session.status === 'idle' || session.status === 'connecting') {
this.elements.headerTitle.textContent = '通话';
} else {
this.elements.headerTitle.textContent = `${session.remoteUser.name} 的通话`;
}
} }
if (this.elements.encryptionBadge) { if (this.elements.encryptionBadge) {
toggleElement(this.elements.encryptionBadge, session.isEncrypted); toggleElement(this.elements.encryptionBadge, session.isEncrypted);
} }
// 始终显示网络状态指示器和质量
if (this.elements.remoteNetworkIndicator) {
this.elements.remoteNetworkIndicator.classList.remove('hidden');
}
if (this.elements.remoteNetworkQuality) {
this.elements.remoteNetworkQuality.classList.remove('hidden');
}
this.renderCallDuration(session.duration); this.renderCallDuration(session.duration);
} }
@@ -193,16 +208,34 @@ class UIRenderer {
// 渲染远端视频 // 渲染远端视频
renderRemoteVideo(remoteUser) { renderRemoteVideo(remoteUser) {
if (this.elements.remoteName) { // 找到远端信息覆盖层元素
this.elements.remoteName.textContent = remoteUser.name; const remoteInfoOverlay = this.elements.remoteName?.closest('.absolute.top-6.left-6.glass');
}
if (this.elements.remoteAvatar) { // 未连接时不显示红框部分
this.elements.remoteAvatar.src = remoteUser.avatar; if (remoteUser.status === 'offline') {
} // 隐藏整个远端信息覆盖层
if (remoteInfoOverlay) {
remoteInfoOverlay.classList.add('hidden');
}
} else {
// 显示远端信息覆盖层
if (remoteInfoOverlay) {
remoteInfoOverlay.classList.remove('hidden');
}
if (this.elements.remoteStatus) { // 连接后更新头像和名称数据
this.elements.remoteStatus.textContent = this.getStatusText(remoteUser.status); if (this.elements.remoteName) {
this.elements.remoteName.textContent = remoteUser.name;
}
if (this.elements.remoteAvatar) {
this.elements.remoteAvatar.src = remoteUser.avatar;
this.elements.remoteAvatar.classList.remove('hidden');
}
if (this.elements.remoteStatus) {
this.elements.remoteStatus.textContent = this.getStatusText(remoteUser.status);
}
} }
// 当远程视频关闭时显示占位符 // 当远程视频关闭时显示占位符
@@ -444,6 +477,17 @@ class UIRenderer {
renderUserList(localUser, remoteUser) { renderUserList(localUser, remoteUser) {
if (!this.elements.userList) return; if (!this.elements.userList) return;
// 计算通话成员总数
let userCount = 1; // 至少有本地用户
if (remoteUser.status === 'online' || remoteUser.status === 'connecting') {
userCount++; // 只有当远程用户在线或连接中时,增加计数
}
// 更新通话成员总数显示
if (this.elements.userCountDisplay) {
this.elements.userCountDisplay.textContent = `通话成员 (${userCount})`;
}
// 渲染本地用户 // 渲染本地用户
const localUserElement = this.elements.userList.querySelector('[data-user-id="local"]'); const localUserElement = this.elements.userList.querySelector('[data-user-id="local"]');
if (localUserElement) { if (localUserElement) {
@@ -486,63 +530,71 @@ class UIRenderer {
// 渲染远程用户 // 渲染远程用户
const remoteUserElement = this.elements.userList.querySelector('[data-user-id="remote"]'); const remoteUserElement = this.elements.userList.querySelector('[data-user-id="remote"]');
if (remoteUserElement) { if (remoteUserElement) {
// 渲染远程用户头像 // 未连接时不显示远程用户信息
const remoteAvatar = remoteUserElement.querySelector('img[data-field="remoteUser.avatar"]'); if (remoteUser.status === 'offline') {
if (remoteAvatar) { remoteUserElement.classList.add('hidden');
remoteAvatar.src = remoteUser.avatar; } else {
} // 连接后显示远程用户信息并更新数据
// 渲染远程用户名字 remoteUserElement.classList.remove('hidden');
const remoteName = remoteUserElement.querySelector('[data-field="remoteUser.name"]');
if (remoteName) { // 渲染远程用户头像
remoteName.textContent = remoteUser.name; const remoteAvatar = remoteUserElement.querySelector('img[data-field="remoteUser.avatar"]');
} if (remoteAvatar) {
// 渲染远程用户媒体状态 remoteAvatar.src = remoteUser.avatar;
const remoteMediaStatus = remoteUserElement.querySelector('[data-field="remoteUser.mediaStatus"]');
if (remoteMediaStatus) {
if (!remoteUser.mediaState.audio) {
remoteMediaStatus.textContent = '静音中';
remoteMediaStatus.className = 'text-xs text-gray-500';
} else if (!remoteUser.mediaState.video) {
remoteMediaStatus.textContent = '视频关闭';
remoteMediaStatus.className = 'text-xs text-gray-500';
} else {
remoteMediaStatus.textContent = '在线';
remoteMediaStatus.className = 'text-xs text-green-400';
} }
} // 渲染远程用户名字
// 渲染远程用户在线状态指示器 const remoteName = remoteUserElement.querySelector('[data-field="remoteUser.name"]');
const remoteStatusIndicator = remoteUserElement.querySelector('.absolute.-bottom-1.-right-1.w-3.h-3'); if (remoteName) {
if (remoteStatusIndicator) { remoteName.textContent = remoteUser.name;
if (remoteUser.status === 'online') { }
// 根据网络质量设置状态指示器颜色 // 渲染远程用户媒体状态
if (remoteUser.networkQuality === 'no_signal') { const remoteMediaStatus = remoteUserElement.querySelector('[data-field="remoteUser.mediaStatus"]');
remoteStatusIndicator.classList.remove('hidden'); if (remoteMediaStatus) {
remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-gray-500 rounded-full border-2 border-slate-900'; if (!remoteUser.mediaState.audio) {
remoteMediaStatus.textContent = '静音中';
remoteMediaStatus.className = 'text-xs text-gray-500';
} else if (!remoteUser.mediaState.video) {
remoteMediaStatus.textContent = '视频关闭';
remoteMediaStatus.className = 'text-xs text-gray-500';
} else { } else {
remoteStatusIndicator.classList.remove('hidden'); remoteMediaStatus.textContent = '在线';
remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900'; remoteMediaStatus.className = 'text-xs text-green-400';
} }
} else {
remoteStatusIndicator.classList.add('hidden');
} }
} // 渲染远程用户在线状态指示器
// 渲染远程用户静音图标 const remoteStatusIndicator = remoteUserElement.querySelector('.absolute.-bottom-1.-right-1.w-3.h-3');
const remoteMuteIcon = remoteUserElement.querySelector('[data-field="remoteUser.muteIcon"]'); if (remoteStatusIndicator) {
if (remoteMuteIcon) { if (remoteUser.status === 'online') {
if (!remoteUser.mediaState.audio) { // 根据网络质量设置状态指示器颜色
remoteMuteIcon.classList.remove('hidden'); if (remoteUser.networkQuality === 'no_signal') {
remoteMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs'; remoteStatusIndicator.classList.remove('hidden');
} else { remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-gray-500 rounded-full border-2 border-slate-900';
remoteMuteIcon.classList.add('hidden'); } else {
remoteStatusIndicator.classList.remove('hidden');
remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900';
}
} else {
remoteStatusIndicator.classList.add('hidden');
}
} }
} // 渲染远程用户静音图标
// 渲染远程用户说话状态指示器 const remoteMuteIcon = remoteUserElement.querySelector('[data-field="remoteUser.muteIcon"]');
const remoteSpeakingIndicator = remoteUserElement.querySelector('[data-field="remoteUser.speakingIndicator"]'); if (remoteMuteIcon) {
if (remoteSpeakingIndicator) { if (!remoteUser.mediaState.audio) {
if (remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio) { remoteMuteIcon.classList.remove('hidden');
remoteSpeakingIndicator.classList.remove('hidden'); remoteMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs';
} else { } else {
remoteSpeakingIndicator.classList.add('hidden'); remoteMuteIcon.classList.add('hidden');
}
}
// 渲染远程用户说话状态指示器
const remoteSpeakingIndicator = remoteUserElement.querySelector('[data-field="remoteUser.speakingIndicator"]');
if (remoteSpeakingIndicator) {
if (remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio) {
remoteSpeakingIndicator.classList.remove('hidden');
} else {
remoteSpeakingIndicator.classList.add('hidden');
}
} }
} }
} }
@@ -786,6 +838,9 @@ class UIRenderer {
// 同步更新头部网络指示器 // 同步更新头部网络指示器
this.updateHeaderNetworkIndicator(quality); this.updateHeaderNetworkIndicator(quality);
// 同步更新头部网络质量文本
this.renderHeaderNetworkStatus(quality);
} }
// 更新头部网络指示器 // 更新头部网络指示器