通话计数和显示隐藏修改完成
This commit is contained in:
@@ -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="对方视频"
|
|
||||||
class="w-full h-full object-contain"
|
|
||||||
autoplay
|
|
||||||
data-field="remoteUser.videoStream">
|
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>
|
||||||
@@ -176,11 +178,12 @@
|
|||||||
<!-- [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"
|
|
||||||
placeholder="输入消息..."
|
|
||||||
class="flex-1 bg-transparent border-none outline-none text-sm text-white placeholder-gray-500 px-2"
|
class="flex-1 bg-transparent border-none outline-none text-sm text-white placeholder-gray-500 px-2"
|
||||||
data-field="chatInput"
|
data-field="chatInput" onkeypress="handleChatSubmit(event)">
|
||||||
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
|
||||||
|
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()">
|
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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
// 未连接时不显示红框部分
|
||||||
|
if (session.status === 'idle' || session.status === 'connecting') {
|
||||||
|
this.elements.headerTitle.textContent = '通话';
|
||||||
|
} else {
|
||||||
this.elements.headerTitle.textContent = `与 ${session.remoteUser.name} 的通话`;
|
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,17 +208,35 @@ class UIRenderer {
|
|||||||
|
|
||||||
// 渲染远端视频
|
// 渲染远端视频
|
||||||
renderRemoteVideo(remoteUser) {
|
renderRemoteVideo(remoteUser) {
|
||||||
|
// 找到远端信息覆盖层元素
|
||||||
|
const remoteInfoOverlay = this.elements.remoteName?.closest('.absolute.top-6.left-6.glass');
|
||||||
|
|
||||||
|
// 未连接时不显示红框部分
|
||||||
|
if (remoteUser.status === 'offline') {
|
||||||
|
// 隐藏整个远端信息覆盖层
|
||||||
|
if (remoteInfoOverlay) {
|
||||||
|
remoteInfoOverlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 显示远端信息覆盖层
|
||||||
|
if (remoteInfoOverlay) {
|
||||||
|
remoteInfoOverlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接后更新头像和名称数据
|
||||||
if (this.elements.remoteName) {
|
if (this.elements.remoteName) {
|
||||||
this.elements.remoteName.textContent = remoteUser.name;
|
this.elements.remoteName.textContent = remoteUser.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.elements.remoteAvatar) {
|
if (this.elements.remoteAvatar) {
|
||||||
this.elements.remoteAvatar.src = remoteUser.avatar;
|
this.elements.remoteAvatar.src = remoteUser.avatar;
|
||||||
|
this.elements.remoteAvatar.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.elements.remoteStatus) {
|
if (this.elements.remoteStatus) {
|
||||||
this.elements.remoteStatus.textContent = this.getStatusText(remoteUser.status);
|
this.elements.remoteStatus.textContent = this.getStatusText(remoteUser.status);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 当远程视频关闭时显示占位符
|
// 当远程视频关闭时显示占位符
|
||||||
if (this.elements.remoteVideoPlaceholder) {
|
if (this.elements.remoteVideoPlaceholder) {
|
||||||
@@ -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,6 +530,13 @@ 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) {
|
||||||
|
// 未连接时不显示远程用户信息
|
||||||
|
if (remoteUser.status === 'offline') {
|
||||||
|
remoteUserElement.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 连接后显示远程用户信息并更新数据
|
||||||
|
remoteUserElement.classList.remove('hidden');
|
||||||
|
|
||||||
// 渲染远程用户头像
|
// 渲染远程用户头像
|
||||||
const remoteAvatar = remoteUserElement.querySelector('img[data-field="remoteUser.avatar"]');
|
const remoteAvatar = remoteUserElement.querySelector('img[data-field="remoteUser.avatar"]');
|
||||||
if (remoteAvatar) {
|
if (remoteAvatar) {
|
||||||
@@ -547,6 +598,7 @@ class UIRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 在renderer.js中添加方法
|
// 在renderer.js中添加方法
|
||||||
// 获取视频流分辨率
|
// 获取视频流分辨率
|
||||||
getVideoResolution(track) {
|
getVideoResolution(track) {
|
||||||
@@ -786,6 +838,9 @@ class UIRenderer {
|
|||||||
|
|
||||||
// 同步更新头部网络指示器
|
// 同步更新头部网络指示器
|
||||||
this.updateHeaderNetworkIndicator(quality);
|
this.updateHeaderNetworkIndicator(quality);
|
||||||
|
|
||||||
|
// 同步更新头部网络质量文本
|
||||||
|
this.renderHeaderNetworkStatus(quality);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新头部网络指示器
|
// 更新头部网络指示器
|
||||||
|
|||||||
Reference in New Issue
Block a user