Files
webRtc/WebApp/client/public/onebyone/renderer.js
2026-04-11 00:03:30 +08:00

913 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* UI渲染器
* 负责将状态映射到DOM与状态管理解耦
*/
import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js';
import { mockCallSession } from './models.js';
import chatMessage from './chatmessage.js';
class UIRenderer {
constructor(stateManager) {
this.stateManager = stateManager;
this.unsubscribe = stateManager.subscribe(this.render.bind(this));
// 缓存 DOM 元素
this.elements = {
// 头部和底部
header: document.querySelector('header'),
footer: document.querySelector('footer'),
// 头部内容
headerTitle: document.getElementById('headerTitle'),
callDuration: document.getElementById('callDuration'),
encryptionBadge: document.getElementById('encryptionBadge'),
unreadBadge: document.getElementById('unreadBadge'),
remoteNetworkIndicator: document.getElementById('remoteNetworkIndicator'),
remoteNetworkQuality: document.getElementById('remoteNetworkQuality'),
// 远端视频
remoteVideo: document.getElementById('remoteVideo'),
remoteVideoPlaceholder: document.getElementById('remoteVideoPlaceholder'),
remoteAvatar: document.getElementById('remoteAvatar'),
remoteName: document.getElementById('remoteName'),
remoteStatus: document.getElementById('remoteStatus'),
remoteSpeakingIndicator: document.getElementById('remoteSpeakingIndicator'),
remoteAudioWave: document.getElementById('remoteAudioWave'),
remoteInfoOverlay: document.querySelector('.absolute.top-6.left-6.glass'),
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'),
localMediaStatus: document.getElementById('localMediaStatus'),
localMuteIcon: document.querySelector('[data-field="localUser.muteIcon"]'),
userCountDisplay: document.getElementById('userCountDisplay'),
// 控制按钮
micBtn: document.getElementById('micBtn'),
videoBtn: document.getElementById('videoBtn'),
recordBtn: document.getElementById('recordBtn'),
connectionQuality: document.getElementById('connectionQuality')
};
// 绑定事件监听器
this.bindEventListeners();
// 订阅状态变化
this.unsubscribe = stateManager.subscribe(this.render.bind(this));
// 订阅消息状态变化
this.messageUnsubscribe = chatMessage.subscribe(this.renderMessageState.bind(this));
// 初始化渲染
this.render(this.stateManager.getState(), { type: 'INIT' });
window.addEventListener('resize', () => {
if (this.elements.remoteVideo && this.elements.remoteVideo.srcObject) {
const stream = this.elements.remoteVideo.srcObject;
const videoTracks = stream.getVideoTracks();
if (videoTracks.length > 0) {
const resolution = this.getVideoResolution(videoTracks[0]);
this.adjustVideoSize(this.elements.remoteVideo, resolution);
}
}
});
}
// 渲染消息状态变化
renderMessageState(messageState, changes) {
switch (changes.type) {
case 'NEW_MESSAGE':
this.renderChatMessages(messageState.messages);
this.renderUnreadCount(changes.unreadCount);
break;
case 'SIDEBAR_TOGGLE':
this.renderSidebar(changes.isOpen);
// 当侧边栏打开时,重置未读消息计数
if (changes.isOpen) {
this.renderUnreadCount(0);
} else {
this.renderUnreadCount(changes.unreadCount);
}
break;
}
}
// 绑定事件监听器
bindEventListeners() {
// 事件监听器
}
// 渲染状态变化
render(state, changes) {
switch (changes.type) {
case 'INIT':
this.renderHeader(state.session);
this.renderRemoteVideo(state.session.remoteUser);
this.renderLocalVideo(state.session.localUser, state.localStream);
this.renderControlButtons(state.session.localUser.mediaState);
this.renderChatMessages(chatMessage.getMessageState().messages);
this.renderUserList(state.session.localUser, state.session.remoteUser);
// 初始化时检查远程流状态,显示或隐藏占位背景
if (this.elements.remoteVideoPlaceholder) {
if (state.remoteStream) {
this.elements.remoteVideoPlaceholder.classList.add('hidden');
} else {
this.elements.remoteVideoPlaceholder.classList.remove('hidden');
}
}
break;
case 'DURATION_UPDATE':
this.renderCallDuration(changes.duration);
break;
case 'LOCAL_MEDIA_CHANGE':
this.renderControlButtons(state.session.localUser.mediaState);
this.renderLocalVideo(state.session.localUser, state.localStream);
this.renderLocalUserStatus(state.session.localUser);
this.renderUserList(state.session.localUser, state.session.remoteUser);
break;
case 'LOCAL_STREAM_OBTAINED':
this.renderLocalStream(state.localStream);
this.renderLocalVideo(state.session.localUser, state.localStream);
break;
case 'REMOTE_STREAM_OBTAINED':
this.renderRemoteStream(state.remoteStream);
// 当获取到远程流时,隐藏连接中提示
if (this.elements.connectingOverlay) {
this.elements.connectingOverlay.classList.add('hidden');
}
break;
case 'REMOTE_MEDIA_CHANGE':
this.renderRemoteVideo(state.session.remoteUser);
this.renderUserList(state.session.localUser, state.session.remoteUser);
break;
case 'USER_LIST_UPDATE':
this.renderUserList(changes.localUser, changes.remoteUser);
break;
case 'NETWORK_CHANGE':
this.renderNetworkStatus(changes.quality);
break;
case 'CALL_STATUS_CHANGE':
this.renderCallStatus(changes.status);
break;
case 'CALL_ENDED':
this.renderCallEnded();
break;
}
}
// 渲染通话状态
renderCallStatus(status) {
if (this.elements.connectingOverlay) {
if (status === 'connecting') {
this.elements.connectingOverlay.classList.remove('hidden');
} else {
this.elements.connectingOverlay.classList.add('hidden');
}
}
}
// 渲染头部
renderHeader(session) {
if (this.elements.headerTitle) {
// 未连接时不显示红框部分
if (session.status === 'idle' || session.status === 'connecting') {
this.elements.headerTitle.textContent = '通话';
} else {
this.elements.headerTitle.textContent = `${session.remoteUser.name} 的通话`;
}
}
if (this.elements.encryptionBadge) {
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);
}
// 渲染通话时长
renderCallDuration(duration) {
if (this.elements.callDuration) {
this.elements.callDuration.textContent = formatTime(duration);
}
}
// 渲染远端视频
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) {
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) {
// 显示与黄框中一致的状态
if (!remoteUser.mediaState.audio) {
this.elements.remoteStatus.textContent = '静音中';
} else if (!remoteUser.mediaState.video) {
this.elements.remoteStatus.textContent = '视频关闭';
} else {
this.elements.remoteStatus.textContent = this.getStatusText(remoteUser.status);
}
}
}
// 同步更新侧边栏用户列表
this.renderUserList(this.stateManager.getState().session.localUser, remoteUser);
// 当远程视频关闭时显示占位符
if (this.elements.remoteVideoPlaceholder) {
const shouldShowPlaceholder = !remoteUser.mediaState.video;
toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder);
// 更新占位符文本内容
if (shouldShowPlaceholder) {
const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center');
if (placeholderContent) {
const titleElement = placeholderContent.querySelector('p.text-white.text-lg.font-medium');
if (titleElement) {
titleElement.textContent = '对方摄像头已关闭';
}
const subtitleElement = placeholderContent.querySelector('p.text-sm.text-gray-400');
if (subtitleElement) {
subtitleElement.textContent = '对方暂时关闭了视频';
}
}
} else {
// 恢复默认占位符文本
const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center');
if (placeholderContent) {
const titleElement = placeholderContent.querySelector('p.text-white.text-lg.font-medium');
if (titleElement) {
titleElement.textContent = '等待对方连接...';
}
const subtitleElement = placeholderContent.querySelector('p.text-sm.text-gray-400');
if (subtitleElement) {
subtitleElement.textContent = '请确保对方已加入通话';
}
}
}
}
// 渲染说话状态
if (this.elements.remoteSpeakingIndicator) {
toggleElement(this.elements.remoteSpeakingIndicator, remoteUser.mediaState.isSpeaking);
}
if (this.elements.remoteAudioWave) {
toggleElement(this.elements.remoteAudioWave, remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio);
}
// 渲染网络状态
this.renderNetworkStatus(remoteUser.networkQuality);
// 渲染header中的网络状态
this.renderHeaderNetworkStatus(remoteUser.networkQuality);
}
// 渲染header中的网络状态
renderHeaderNetworkStatus(networkQuality) {
if (this.elements.remoteNetworkQuality) {
const textElement = this.elements.remoteNetworkQuality.querySelector('span');
const iconElement = this.elements.remoteNetworkQuality.querySelector('i');
if (textElement && iconElement) {
let qualityText = '未知';
let iconClass = 'fas fa-signal text-gray-400';
switch (networkQuality) {
case 'excellent':
qualityText = '优秀';
iconClass = 'fas fa-signal text-green-400';
break;
case 'good':
qualityText = '良好';
iconClass = 'fas fa-signal text-green-500';
break;
case 'fair':
qualityText = '一般';
iconClass = 'fas fa-signal text-yellow-400';
break;
case 'poor':
qualityText = '较差';
iconClass = 'fas fa-signal text-red-400';
break;
}
textElement.textContent = qualityText;
iconElement.className = iconClass;
}
}
}
// 渲染本地视频
renderLocalVideo(localUser, localStream) {
if (this.elements.localVideoPlaceholder) {
// 当没有视频流或视频关闭时显示占位符
const shouldShowPlaceholder = !localStream || !localUser.mediaState.video;
toggleElement(this.elements.localVideoPlaceholder, shouldShowPlaceholder);
}
if (this.elements.localAudioWave) {
toggleElement(this.elements.localAudioWave, localUser.mediaState.isSpeaking);
}
// 同时渲染本地用户状态
this.renderLocalUserStatus(localUser);
}
// 渲染本地视频流
renderLocalStream(stream) {
if (this.elements.localVideo && stream) {
this.elements.localVideo.srcObject = stream;
this.elements.localVideo.autoplay = true;
this.elements.localVideo.muted = true; // 本地视频静音,避免回声
console.log('srcObject set successfully:', this.elements.localVideo.srcObject);
// 隐藏断开连接覆盖层
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.add('hidden');
}
} else {
console.error('Either localVideo element or stream is missing');
}
}
// 渲染远程视频流
renderRemoteStream(stream) {
if (this.elements.remoteVideo && stream) {
console.log('Rendering remote stream:', stream);
// 即使流对象相同,也要重新设置,确保视频元素能够识别轨道变化
this.elements.remoteVideo.srcObject = null;
// 延迟设置srcObject确保视频元素能够正确处理
setTimeout(() => {
this.elements.remoteVideo.srcObject = stream;
console.log('Remote stream reset successfully:', stream);
// 确保视频元素的属性正确设置
this.elements.remoteVideo.autoplay = true;
this.elements.remoteVideo.playsinline = true;
this.elements.remoteVideo.muted = false; // 不要静音远程视频,否则听不到对方的声音
// 关键设置:启用硬件加速和最佳质量渲染
this.elements.remoteVideo.style.transform = 'translateZ(0)'; // 启用硬件加速
this.elements.remoteVideo.style.imageRendering = 'pixelated'; // 保持像素清晰
this.elements.remoteVideo.style.objectFit = 'contain'; // 保持比例
// 隐藏断开连接覆盖层
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.add('hidden');
}
// 获取视频轨道并处理分辨率
const videoTracks = stream.getVideoTracks();
console.log('Remote video tracks:', videoTracks);
// 检查是否有有效的视频轨道
const hasValidVideoTrack = videoTracks.length > 0 && videoTracks.some(track => {
// 检查轨道是否已停止或被禁用
return track.readyState === 'live';
});
console.log('Has valid video track:', hasValidVideoTrack);
if (hasValidVideoTrack) {
console.log('Found valid video tracks, updating resolution');
const activeVideoTrack = videoTracks.find(track => track.readyState === 'live');
if (activeVideoTrack) {
const resolution = this.getVideoResolution(activeVideoTrack);
this.adjustVideoSize(this.elements.remoteVideo, resolution);
// 监听轨道变化,处理分辨率调整
activeVideoTrack.addEventListener('resize', () => {
const newResolution = this.getVideoResolution(activeVideoTrack);
this.adjustVideoSize(this.elements.remoteVideo, newResolution);
});
}
// 隐藏连接中提示
if (this.elements.connectingOverlay) {
this.elements.connectingOverlay.classList.add('hidden');
}
// 隐藏占位背景
if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.add('hidden');
}
} else {
console.log('No valid video tracks in remote stream');
// 清空视频元素的源
this.elements.remoteVideo.srcObject = null;
// 显示占位背景
if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.remove('hidden');
}
}
}, 50); // 增加延迟时间,确保视频元素有足够的时间处理
} else {
console.error('Either remoteVideo element or stream is missing');
// 清空视频元素的源
if (this.elements.remoteVideo) {
this.elements.remoteVideo.srcObject = null;
}
// 显示占位背景
if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.remove('hidden');
}
}
}
// 渲染本地用户状态
renderLocalUserStatus(localUser) {
// 更新本地媒体状态文本
if (this.elements.localMediaStatus) {
if (!localUser.mediaState.audio) {
this.elements.localMediaStatus.textContent = '静音中';
this.elements.localMediaStatus.className = 'text-xs text-gray-500';
} else if (!localUser.mediaState.video) {
this.elements.localMediaStatus.textContent = '视频关闭';
this.elements.localMediaStatus.className = 'text-xs text-gray-500';
} else {
this.elements.localMediaStatus.textContent = '在线';
this.elements.localMediaStatus.className = 'text-xs text-green-400';
}
}
// 更新静音图标
if (this.elements.localMuteIcon) {
if (!localUser.mediaState.audio) {
this.elements.localMuteIcon.classList.remove('hidden');
this.elements.localMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs';
} else {
this.elements.localMuteIcon.classList.add('hidden');
}
}
}
// 渲染侧边栏用户列表
renderUserList(localUser, remoteUser) {
if (!this.elements.userList) return;
// 计算通话成员总数
let userCount = 1; // 至少有本地用户
if (remoteUser.status !== 'offline') {
userCount++; // 如果远程用户在线,增加计数
}
// 更新通话成员总数显示
const userCountElement = this.elements.userList.closest('div').querySelector('h3.text-sm.font-medium.text-gray-400');
if (userCountElement) {
userCountElement.textContent = `通话成员 (${userCount})`;
}
// 渲染本地用户
const localUserElement = this.elements.userList.querySelector('[data-user-id="local"]');
if (localUserElement) {
// 渲染本地用户头像
const localAvatar = localUserElement.querySelector('img[data-field="localUser.avatar"]');
if (localAvatar) {
localAvatar.src = localUser.avatar;
}
// 渲染本地用户名字
const localName = localUserElement.querySelector('[data-field="localUser.name"]');
if (localName) {
localName.textContent = localUser.name;
}
// 渲染本地用户媒体状态
const localMediaStatus = localUserElement.querySelector('[data-field="localUser.mediaStatus"]');
if (localMediaStatus) {
if (!localUser.mediaState.audio) {
localMediaStatus.textContent = '静音中';
localMediaStatus.className = 'text-xs text-gray-500';
} else if (!localUser.mediaState.video) {
localMediaStatus.textContent = '视频关闭';
localMediaStatus.className = 'text-xs text-gray-500';
} else {
localMediaStatus.textContent = '在线';
localMediaStatus.className = 'text-xs text-green-400';
}
}
// 渲染本地用户静音图标
const localMuteIcon = localUserElement.querySelector('[data-field="localUser.muteIcon"]');
if (localMuteIcon) {
if (!localUser.mediaState.audio) {
localMuteIcon.classList.remove('hidden');
localMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs';
} else {
localMuteIcon.classList.add('hidden');
}
}
}
// 渲染远程用户
const remoteUserElement = this.elements.userList.querySelector('[data-user-id="remote"]');
if (remoteUserElement) {
// 未连接时不显示远程用户信息
if (remoteUser.status === 'offline') {
remoteUserElement.classList.add('hidden');
} else {
// 连接后显示远程用户信息并更新数据
remoteUserElement.classList.remove('hidden');
// 渲染远程用户头像
const remoteAvatar = remoteUserElement.querySelector('img[data-field="remoteUser.avatar"]');
if (remoteAvatar) {
remoteAvatar.src = remoteUser.avatar;
}
// 渲染远程用户名字
const remoteName = remoteUserElement.querySelector('[data-field="remoteUser.name"]');
if (remoteName) {
remoteName.textContent = remoteUser.name;
}
// 渲染远程用户媒体状态
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 remoteStatusIndicator = remoteUserElement.querySelector('.absolute.-bottom-1.-right-1.w-3.h-3');
if (remoteStatusIndicator) {
if (remoteUser.status === 'online') {
// 根据网络质量设置状态指示器颜色
if (remoteUser.networkQuality === 'no_signal') {
remoteStatusIndicator.classList.remove('hidden');
remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-gray-500 rounded-full border-2 border-slate-900';
} 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"]');
if (remoteMuteIcon) {
if (!remoteUser.mediaState.audio) {
remoteMuteIcon.classList.remove('hidden');
remoteMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs';
} else {
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');
}
}
}
}
}
// 在renderer.js中添加方法
// 获取视频流分辨率
getVideoResolution(track) {
if (track && track.getSettings) {
const settings = track.getSettings();
return {
width: settings.width || 640,
height: settings.height || 480
};
}
return { width: 640, height: 480 }; // 默认值
}
// 调整视频元素大小并居中显示
adjustVideoSize(videoElement, resolution) {
if (!videoElement) return;
const { width, height } = resolution;
const aspectRatio = width / height;
// 根据容器大小和视频宽高比调整视频显示
const container = videoElement.parentElement;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
// 启用硬件加速
videoElement.style.transform = 'translateZ(0)';
videoElement.style.willChange = 'transform';
// 设置容器为flex布局使视频居中
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.justifyContent = 'center';
// 优化图像渲染
videoElement.style.imageRendering = 'pixelated';
// 确保视频元素在容器内正确显示
videoElement.style.maxWidth = '100%';
videoElement.style.maxHeight = '100%';
videoElement.style.objectFit = 'contain'; // 保持原始比例,不裁剪
}
// 渲染控制按钮
renderControlButtons(mediaState) {
if (this.elements.micBtn) {
toggleButtonState(this.elements.micBtn, !mediaState.audio);
}
if (this.elements.videoBtn) {
toggleButtonState(this.elements.videoBtn, !mediaState.video);
}
if (this.elements.recordBtn) {
toggleButtonState(this.elements.recordBtn, mediaState.recording);
}
}
// 渲染聊天消息
renderChatMessages(messages) {
if (!this.elements.chatContent) return;
// 清空聊天内容
this.elements.chatContent.innerHTML = '';
// 添加通话开始时间
const startTimeElement = document.createElement('div');
startTimeElement.className = 'text-center text-xs text-gray-500 my-4';
const startTime = messages[0]?.timestamp || new Date().toISOString();
startTimeElement.textContent = `通话开始 ${formatTimestamp(startTime)}`;
this.elements.chatContent.appendChild(startTimeElement);
// 添加消息
messages.forEach(message => {
const messageElement = this.createMessageElement(message);
this.elements.chatContent.appendChild(messageElement);
});
// 滚动到底部
this.elements.chatContent.scrollTop = this.elements.chatContent.scrollHeight;
}
// 创建消息元素
createMessageElement(message) {
const messageDiv = document.createElement('div');
// 根据消息类型设置不同的CSS类
let messageClass = 'chat-bubble';
if (message.type === 'system') {
messageClass += ' message-system';
} else if (message.isSelf) {
messageClass += ' message-self';
} else {
messageClass += ' message-other';
}
messageDiv.className = messageClass;
messageDiv.dataset.messageId = message.id;
let contentHTML = '';
if (message.type === 'file' && message.content.startsWith('data:image/')) {
// 图片消息
contentHTML = `
<div class="message-image-container">
<img src="${message.content}" class="message-image" alt="${message.fileName || '图片'}">
${message.fileName ? `<div class="message-image-name">${message.fileName}</div>` : ''}
</div>
`;
} else {
// 文本消息
contentHTML = `
<div class="message-text">
${message.content}
</div>
`;
}
messageDiv.innerHTML = `
<div class="message-header">
<img src="${message.senderAvatar}" class="message-avatar">
<div>
<span class="message-sender">${message.senderName}</span>
<span class="message-time">${formatTimestamp(message.timestamp)}</span>
</div>
</div>
<div class="message-content">
${contentHTML}
</div>
`;
return messageDiv;
}
// 渲染未读消息数
renderUnreadCount(count) {
if (this.elements.unreadBadge) {
if (count > 0) {
this.elements.unreadBadge.textContent = count;
this.elements.unreadBadge.classList.remove('hidden');
} else {
this.elements.unreadBadge.classList.add('hidden');
}
}
}
// 渲染侧边栏
renderSidebar(isOpen) {
if (this.elements.sidebar) {
if (isOpen) {
this.elements.sidebar.classList.remove('hidden');
} else {
this.elements.sidebar.classList.add('hidden');
}
}
}
// 渲染网络状态
renderNetworkStatus(quality) {
if (this.elements.networkStatus && this.elements.networkStatusText) {
// 始终显示网络状态
toggleElement(this.elements.networkStatus, true);
// 根据网络质量设置不同的图标和颜色
const networkStatus = this.elements.networkStatus;
const networkStatusText = this.elements.networkStatusText;
// 清除之前的图标
const existingIcon = networkStatus.querySelector('i');
if (existingIcon) {
existingIcon.remove();
}
// 创建新的图标元素
const icon = document.createElement('i');
// 根据网络质量设置图标和样式
switch (quality) {
case 'excellent':
icon.className = 'fas fa-check-circle text-green-400';
networkStatusText.textContent = this.getNetworkQualityText(quality);
networkStatusText.className = 'text-green-400';
break;
case 'good':
icon.className = 'fas fa-signal text-blue-400';
networkStatusText.textContent = this.getNetworkQualityText(quality);
networkStatusText.className = 'text-blue-400';
break;
case 'fair':
icon.className = 'fas fa-exclamation-circle text-yellow-500';
networkStatusText.textContent = this.getNetworkQualityText(quality);
networkStatusText.className = 'text-yellow-500';
break;
case 'poor':
icon.className = 'fas fa-exclamation-triangle text-red-500';
networkStatusText.textContent = this.getNetworkQualityText(quality);
networkStatusText.className = 'text-red-500';
break;
case 'no_signal':
icon.className = 'fas fa-times-circle text-gray-500';
networkStatusText.textContent = this.getNetworkQualityText(quality);
networkStatusText.className = 'text-gray-500';
break;
default:
icon.className = 'fas fa-question-circle text-gray-400';
networkStatusText.textContent = '未知';
networkStatusText.className = 'text-gray-400';
}
// 添加图标到网络状态元素
networkStatus.insertBefore(icon, networkStatusText);
}
if (this.elements.connectionQuality) {
const qualityText = this.getNetworkQualityText(quality);
let statusClass = '';
// 根据网络质量设置文本颜色
switch (quality) {
case 'excellent':
statusClass = 'text-green-400';
break;
case 'good':
statusClass = 'text-blue-400';
break;
case 'fair':
statusClass = 'text-yellow-500';
break;
case 'poor':
statusClass = 'text-red-500';
break;
case 'no_signal':
statusClass = 'text-gray-500';
break;
default:
statusClass = 'text-gray-400';
}
// 更新连接质量文本和样式
this.elements.connectionQuality.textContent = `连接质量: ${qualityText}`;
this.elements.connectionQuality.className = `text-xs ${statusClass}`;
}
// 同步更新头部网络指示器
this.updateHeaderNetworkIndicator(quality);
// 同步更新头部网络质量文本
this.renderHeaderNetworkStatus(quality);
}
// 更新头部网络指示器
updateHeaderNetworkIndicator(networkQuality) {
if (!this.elements.remoteNetworkIndicator) return;
// 根据网络质量设置指示器颜色
if (networkQuality === 'no_signal') {
// 无信号时显示灰色点,取消动画
this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full';
} else {
// 有信号时显示绿色点,保持动画
this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-green-500 rounded-full animate-pulse';
}
}
// 渲染通话结束
renderCallEnded() {
console.log('Call ended');
// 跳转到结束通话界面
window.location.href = './endcall/endcall.html';
}
// 获取状态文本
getStatusText(status) {
const statusMap = {
'online': '在线',
'offline': '离线',
'connecting': '连接中'
};
return statusMap[status] || status;
}
// 获取网络质量文本
getNetworkQualityText(quality) {
const qualityMap = {
'excellent': '优秀',
'good': '良好',
'fair': '一般',
'poor': '较差',
'no_signal': '无信号'
};
return qualityMap[quality] || quality;
}
// 销毁
destroy() {
if (this.unsubscribe) {
this.unsubscribe();
}
if (this.messageUnsubscribe) {
this.messageUnsubscribe();
}
}
}
export default UIRenderer;