From c89b22d3206b4a953c650e8d8996d3913bd16657 Mon Sep 17 00:00:00 2001 From: stary <834207172@qq.com> Date: Sun, 24 May 2026 13:29:54 +0800 Subject: [PATCH] ++ --- client/public/renderer-ui.js | 189 +++++++++++ client/public/renderer.js | 621 +++++++++++------------------------ 2 files changed, 373 insertions(+), 437 deletions(-) create mode 100644 client/public/renderer-ui.js diff --git a/client/public/renderer-ui.js b/client/public/renderer-ui.js new file mode 100644 index 0000000..6990cb6 --- /dev/null +++ b/client/public/renderer-ui.js @@ -0,0 +1,189 @@ +const DEFAULT_NETWORK_QUALITY = { + label: '\u672a\u77e5', + statusIconClass: 'fas fa-question-circle text-gray-400', + statusTextClass: 'text-gray-400', + headerIconClass: 'fas fa-signal text-gray-400', + indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse', + connectionTextClass: 'text-gray-400' +}; + +const NETWORK_QUALITY_DISPLAY = { + excellent: { + label: '\u4f18\u79c0', + statusIconClass: 'fas fa-check-circle text-green-400', + statusTextClass: 'text-green-400', + headerIconClass: 'fas fa-signal text-green-400', + indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse', + connectionTextClass: 'text-green-400' + }, + good: { + label: '\u826f\u597d', + statusIconClass: 'fas fa-signal text-blue-400', + statusTextClass: 'text-blue-400', + headerIconClass: 'fas fa-signal text-green-500', + indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse', + connectionTextClass: 'text-blue-400' + }, + fair: { + label: '\u4e00\u822c', + statusIconClass: 'fas fa-exclamation-circle text-yellow-500', + statusTextClass: 'text-yellow-500', + headerIconClass: 'fas fa-signal text-yellow-400', + indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse', + connectionTextClass: 'text-yellow-500' + }, + poor: { + label: '\u8f83\u5dee', + statusIconClass: 'fas fa-exclamation-triangle text-red-500', + statusTextClass: 'text-red-500', + headerIconClass: 'fas fa-signal text-red-400', + indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse', + connectionTextClass: 'text-red-500' + }, + no_signal: { + label: '\u65e0\u4fe1\u53f7', + statusIconClass: 'fas fa-times-circle text-gray-500', + statusTextClass: 'text-gray-500', + headerIconClass: 'fas fa-signal text-gray-400', + indicatorClass: 'w-2 h-2 bg-gray-500 rounded-full', + connectionTextClass: 'text-gray-500' + } +}; + +function getRoleTagMarkup(user, role) { + if (role === 'local') { + return user.isHost + ? '\u4e3b\u6301\u4eba' + : '\u53c2\u4e0e\u8005'; + } + + if (role === 'participant') { + return '\u53c2\u4e0e\u8005'; + } + + return '\u4e3b\u6301\u4eba'; +} + +function getDatasetUserId(role, id) { + switch (role) { + case 'local': + return 'local'; + case 'remote': + return 'remote'; + case 'host': + return `host_${id}`; + case 'participant': + return `participant_${id}`; + default: + return role; + } +} + +function getAvatarMarkup(user, role) { + if (role === 'local') { + return ``; + } + + return ` +
+ +
+
+ `; +} + +function getRightMarkup(mediaState, role, muteIconMarkup) { + if (role !== 'participant') { + return muteIconMarkup; + } + + const speakingMarkup = (mediaState.isSpeaking && mediaState.audio) + ? '
' + : ''; + + return ` +
+ ${muteIconMarkup} + ${speakingMarkup} +
+ `; +} + +export function getCallTitle(connectionId) { + return `\u901a\u8bdd (${connectionId || ''})`; +} + +export function getRemoteVideoPlaceholderText(isVideoEnabled) { + return isVideoEnabled + ? { + title: '\u7b49\u5f85\u5bf9\u65b9\u8fde\u63a5...', + subtitle: '\u8bf7\u786e\u8ba4\u5bf9\u65b9\u5df2\u52a0\u5165\u901a\u8bdd' + } + : { + title: '\u5bf9\u65b9\u6444\u50cf\u5934\u5df2\u5173\u95ed', + subtitle: '\u5bf9\u65b9\u6682\u65f6\u5173\u95ed\u4e86\u89c6\u9891' + }; +} + +export function getNetworkQualityDisplay(quality) { + return NETWORK_QUALITY_DISPLAY[quality] || DEFAULT_NETWORK_QUALITY; +} + +export function getMediaStatusMeta(mediaState) { + if (!mediaState.audio) { + return { + text: '\u9759\u97f3\u4e2d', + className: 'text-xs text-gray-500', + muteIconClass: 'fas fa-microphone-slash text-gray-500 text-xs', + showMuteIcon: true + }; + } + + if (!mediaState.video) { + return { + text: '\u89c6\u9891\u5173\u95ed', + className: 'text-xs text-gray-500', + muteIconClass: 'fas fa-microphone-slash text-gray-500 text-xs', + showMuteIcon: false + }; + } + + return { + text: '\u5728\u7ebf', + className: 'text-xs text-green-400', + muteIconClass: 'fas fa-microphone-slash text-gray-500 text-xs', + showMuteIcon: false + }; +} + +export function buildUserCountLabel(userCount) { + return `\u901a\u8bdd\u6210\u5458 (${userCount})`; +} + +export function createUserEntryElement({ user, role, id }) { + const entry = document.createElement('div'); + const mediaMeta = getMediaStatusMeta(user.mediaState); + const muteIconMarkup = mediaMeta.showMuteIcon + ? `` + : ''; + const baseClass = 'flex items-center gap-3 p-2 rounded-lg'; + const dataFieldAttr = role === 'local' ? ' data-field="localUser.mediaStatus"' : ''; + + entry.className = role === 'local' + ? `${baseClass} hover:bg-white/5` + : `${baseClass} bg-white/5`; + entry.dataset.userId = getDatasetUserId(role, id); + entry.innerHTML = ` + ${getAvatarMarkup(user, role)} +
+
+ ${user.name} + ${getRoleTagMarkup(user, role)} +
+
${mediaMeta.text}
+
+ ${getRightMarkup(user.mediaState, role, muteIconMarkup)} + `; + + return entry; +} diff --git a/client/public/renderer.js b/client/public/renderer.js index 9ff7af8..36f287f 100644 --- a/client/public/renderer.js +++ b/client/public/renderer.js @@ -1,11 +1,19 @@ /** - * UI渲染器 - * 负责将状态映射到DOM,与状态管理解耦 + * UI婵炴挸寮堕悡瀣闯? + * 閻犳劗鍠曢惌妤冧焊閸℃瑥笑闁诡兛鐒﹀Σ褏浜搁崟顐㈢厒DOM闁挎稑濂旂粭宀勬偐閼哥鍋撴担渚悁闁荤偛妫滆闁? */ import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js'; import { mockCallSession } from './models.js'; import chatMessage from './chatmessage.js'; import store from './store.js'; +import { + buildUserCountLabel, + createUserEntryElement, + getCallTitle, + getMediaStatusMeta, + getNetworkQualityDisplay, + getRemoteVideoPlaceholderText +} from './renderer-ui.js'; const GRID_LAYOUT = { maxColumns: 3, @@ -30,14 +38,14 @@ class UIRenderer { this.stateManager = stateManager; this.unsubscribe = stateManager.subscribe(this.render.bind(this)); - // 缓存 DOM 元素 + // 缂傚倹鎸搁悺?DOM 闁稿繐鍟扮粈? this.elements = { - // 头部和底部 + // 濠㈣埖鎸抽崕鎾椽鐏炵晫淇洪梺? header: document.querySelector('header'), footer: document.querySelector('footer'), - // 多Participant视频网格 + // 濠㈣埖鐡峚rticipant閻熸瑥妫濋。鍓佺磾閹寸偟澹? participantGrid: document.getElementById('participantGrid'), - // 头部内容 + // 濠㈣埖鎸抽崕鎾礃閸涱収鍟? headerTitle: document.getElementById('headerTitle'), callDuration: document.getElementById('callDuration'), encryptionBadge: document.getElementById('encryptionBadge'), @@ -45,41 +53,41 @@ class UIRenderer { remoteNetworkIndicator: document.getElementById('remoteNetworkIndicator'), remoteNetworkQuality: document.getElementById('remoteNetworkQuality'), - // 远端视频 + // 閺夆晜绮庨顒傛喆閸℃侗鏆? remoteVideo: document.getElementById('remoteVideo'), remoteVideoPlaceholder: document.getElementById('remoteVideoPlaceholder'), 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') }; - // 绑定事件监听器 + // 缂備焦鍨甸悾鐐鐎b晜顐介柣鈺傚灥閹宕? 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', () => { @@ -94,7 +102,7 @@ class UIRenderer { }); } - // 渲染消息状态变化 + // 婵炴挸寮堕悡瀣槈閸喍绱栭柣妯垮煐閳ь兛绀佽ぐ澶愬礌? renderMessageState(messageState, changes) { switch (changes.type) { case 'NEW_MESSAGE': @@ -103,7 +111,7 @@ class UIRenderer { break; case 'SIDEBAR_TOGGLE': this.renderSidebar(changes.isOpen); - // 当侧边栏打开时,重置未读消息计数 + // 鐟滅増鎸烽弲鑸垫綇鐟欏嫮鍩夐柟鍨尭缁辨垿寮拋鍦闂佹彃绉堕悿鍡涘嫉椤忓浂鍤㈡繛鎴濈墛娴煎懐鎷嬮埄鍐╂ if (changes.isOpen) { this.renderUnreadCount(0); } else { @@ -113,107 +121,107 @@ class UIRenderer { } } - // 绑定事件监听器 + // 缂備焦鍨甸悾鐐鐎b晜顐介柣鈺傚灥閹宕? bindEventListeners() { - // 事件监听器 + // 濞存粌顑勫▎銏ゆ儎閹存繃鍎旈柛? } /** - * 渲染方法 - 根据状态变化更新UI - * @param {Object} state - 当前应用状态 - * @param {Object} changes - 状态变化对象 + * 婵炴挸寮堕悡瀣棘鐟欏嫮銆?- 闁哄秷顫夊畵渚€鎮╅懜纰樺亾娴g缍侀柛鏍ㄧ墬濞插潡寮惂鐕? + * @param {Object} state - 鐟滅増鎸告晶鐘虫償閺冨倹鏆忛柣妯垮煐閳? + * @param {Object} changes - 闁绘鍩栭埀顑跨瑜板宕犻弽褜鍤犻悹? */ render(state, changes) { - // 根据变化类型执行不同的渲染操作 + // 闁哄秷顫夊畵渚€宕eΟ鍝勵嚙缂侇偉顕ч悗鐑藉箥瑜戦、鎴炵▔瀹ュ懏鍊遍柣銊ュ鐟曞棝寮婚幘瀛樻儥濞? switch (changes.type) { case 'INIT': - // 初始化渲染 - 渲染所有UI元素 - 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, state.participants); // 渲染用户列表 - 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, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵? + this.renderHeader(state.session); // 婵炴挸寮堕悡瀣緞閹绢喖鍔ュǎ鍥e墲娴? + // 闁告帗绻傞~鎰板礌閺嶃劍顦ф俊顐熷亾闁哄被鍎寸换娆戠矙鐎n偆銈﹂柣妯垮煐閳ь兛绶ょ槐婵嬪及閸撗佷粵闁瑰瓨鐗犲▓锝夋寠韫囨挸绐楀ù锝呯Х閸庢寮? if (this.elements.remoteVideoPlaceholder) { if (state.remoteStream) { - this.elements.remoteVideoPlaceholder.classList.add('hidden'); // 有远程流时隐藏占位背景 + this.elements.remoteVideoPlaceholder.classList.add('hidden'); // 闁哄牆顦崇换娆戠矙鐎n偆銈﹂柡鍐ㄧ埣濞堬綁鎸婅箛鎾崇獥濞达絽绉烽崕妤呭疾? } else { - this.elements.remoteVideoPlaceholder.classList.remove('hidden'); // 无远程流时显示占位背景 + this.elements.remoteVideoPlaceholder.classList.remove('hidden'); // 闁哄啰濮剧换娆戠矙鐎n偆銈﹂柡鍐煐濡绮堥崫鍕獥濞达絽绉烽崕妤呭疾? } } break; case 'DURATION_UPDATE': - // 通话时长更新 - 渲染通话时长 + // 闂侇偅淇洪惁浠嬪籍閸洘姣愰柡鍥х摠閺?- 婵炴挸寮堕悡瀣焻濮樺磭妯堥柡鍐ㄧ埣閺? this.renderCallDuration(changes.duration); break; case 'LOCAL_MEDIA_CHANGE': - // 本地媒体状态变化 - 更新相关UI - 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, state.participants); // 渲染用户列表 + // 闁哄牜鍓欏﹢瀛樺垔閹哄秶绉奸柣妯垮煐閳ь兛绀佽ぐ澶愬礌?- 闁哄洤鐡ㄩ弻濠囨儎缁嬪灝褰燯I + 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, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵? break; case 'LOCAL_STREAM_OBTAINED': this.renderLocalStream(state.localStream); this.renderLocalVideo(state.session.localUser, state.localStream); break; case 'REMOTE_STREAM_OBTAINED': - // 远程流获取成功 - 更新远程视频显示 - this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost); // 渲染远程流 - // 当获取到远程流时,隐藏连接中提示 + // 閺夆晜绮庨埢鐓幟规担钘夌闁告瑦鐗楅崹姘跺礉?- 闁哄洤鐡ㄩ弻濠冩交濠婂應鏌ら悷娆忔椤e爼寮伴崜褋浠? + this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost); // 婵炴挸寮堕悡瀣交濠婂應鏌ゆ繛? + // 鐟滅増鎹侀獮蹇涘矗閺嵮冪厒閺夆晜绮庨埢鐓幟规担瑙勵槯闁挎稑鐭傚▓锝夋寠韫囨氨绠鹃柟鎭掑劙閼垫垿骞撻幇顔轰粵 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, state.participants); // 渲染用户列表 - // Host端:精准更新发送者participant tile的占位背景 - // 只有Host端需要处理participantId对应的tile占位符(Participant端没有其他Participant的视频流) + // 閺夆晜绮庨埢鍏煎垔閹哄秶绉奸柣妯垮煐閳ь兛绀佽ぐ澶愬礌?- 闁哄洤鐡ㄩ弻濠冩交濠婂應鏌ら悷娆忔椤e爼宕畝鈧弫銈夊箣瀹勬澘鐏欓悶? + this.renderRemoteVideo(state.session.remoteUser); // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤? + this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵? + // Host缂佹棏鍨界槐鎵垝閹冩珯闁哄洤鐡ㄩ弻濠囧矗閹达腹鍋撴担瑙e亾閸楃瀽rticipant tile闁汇劌瀚畷鐗堟媴瀹ュ牆鍓归柡? + // 闁告瑯浜濆﹢涓爋st缂佹棏鍨跺〒鍓佹啺娴e壊妲遍柣鐐叉晙articipantId閻庣數鎳撶花鏌ユ儍閸撳檮le闁告濮崇紞鍛箔閿旇偐绀凱articipant缂佹棏鍨遍惀鍛村嫉婢跺﹤寰撳ù鐘虫耿articipant闁汇劌瀚~瀣紣閹寸偟銈﹂柨? if (changes.participantId && state.session.localUser.isHost) { - // 从participants Map中读取该participant的video状态,而非remoteUser(多Participant场景remoteUser不精确) + // 濞寸姴绐媋rticipants Map濞戞搩鍙€椤曚即宕i弽顒夊殙participant闁汇劌澧榠deo闁绘鍩栭埀顑跨筏缁辨繈鎳撳畝鍕remoteUser闁挎稑鐗嗛ˇ绺媋rticipant闁革妇鍎ゅ▍妾檈moteUser濞戞挸绉剁花璺ㄦ兜椤曞棛绀? const pInfo = state.participants[changes.participantId]; const showPlaceholder = pInfo ? !pInfo.mediaState.video : true; this.renderParticipantVideoPlaceholder(changes.participantId, showPlaceholder); } break; case 'USER_LIST_UPDATE': - // 用户列表更新 - 重新渲染用户列表 + // 闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧啴寮寸€涙ɑ鐓€ - 闂佹彃绉甸弻濠傘€掗崣澶屽帬闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧? this.renderUserList(changes.localUser, changes.remoteUser, state.participants); break; case 'PARTICIPANTS_UPDATE': - // Participants信息变化 - 重新渲染用户列表并同步tile名称 + // Participants濞e洠鍓濇导鍛村矗濡搫顕?- 闂佹彃绉甸弻濠傘€掗崣澶屽帬闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧啴鐛捄鐑樺€辨慨婵愭ile闁告艾绉惰ⅷ this.renderUserList(state.session.localUser, state.session.remoteUser, changes.participants || state.participants); - // 同步更新participant tile的名称标签 + // 闁告艾鏈鐐哄即鐎涙ɑ鐓€participant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒? this.syncParticipantTileNames(changes.participants || state.participants); break; case 'NETWORK_CHANGE': - // 网络状态变化 - 渲染网络状态 + // 缂傚啯鍨圭划鍫曟偐閼哥鍋撴担绋跨秮闁?- 婵炴挸寮堕悡瀣磾閹寸姷鎹曢柣妯垮煐閳? this.renderNetworkStatus(changes.quality); break; case 'CALL_STATUS_CHANGE': - // 通话状态变化 - 渲染通话状态 + // 闂侇偅淇洪惁浠嬫偐閼哥鍋撴担绋跨秮闁?- 婵炴挸寮堕悡瀣焻濮樺磭妯堥柣妯垮煐閳? this.renderCallStatus(changes.status); break; case 'CALL_ENDED': - // 通话结束 - 渲染通话结束界面 + // 闂侇偅淇洪惁鐣岀磼閹惧瓨灏?- 婵炴挸寮堕悡瀣焻濮樺磭妯堢紓浣规尰濞碱偊鎮惧畝鍕〃 this.renderCallEnded(); break; case 'PARTICIPANT_LEFT': - // participant离开 - 更新UI但房间仍然存在 + // participant缂佸倽顕х槐?- 闁哄洤鐡ㄩ弻濂濞达絽妫欓崺褔姊荤紙鐘电煗闁绘帟娉涢悺銊╁捶? this.renderParticipantLeft(changes.connectionId); break; case 'RESOLUTION_CHANGED': - // 分辨率变化 - 更新UI中的分辨率选中状态 + // 闁告帒妫滄ご鎼佹偝閸パ冪秮闁?- 闁哄洤鐡ㄩ弻濂濞戞搩鍘惧▓鎴﹀礆閸℃岸鍝洪柣婊冩喘閳ь剙顦懙鎴︽偐閼哥鍋? this.renderResolutionChanged(changes.resolution); break; } } - // 渲染通话状态 + // 婵炴挸寮堕悡瀣焻濮樺磭妯堥柣妯垮煐閳? renderCallStatus(status) { if (this.elements.connectingOverlay) { if (status === 'connecting') { @@ -224,7 +232,7 @@ class UIRenderer { } } - // 渲染头部 + // 婵炴挸寮堕悡瀣緞閹绢喖鍔? renderHeader(session) { this.renderHeaderTitle(); @@ -232,7 +240,7 @@ class UIRenderer { toggleElement(this.elements.encryptionBadge, session.isEncrypted); } - // 始终显示网络状态指示器和质量 + // 濠殿喖顑囩划鎾诲及閸撗佷粵缂傚啯鍨圭划鍫曟偐閼哥鍋撴担鐟扮樄缂佲偓閸濆嫭鐝ら柛婊冪焷瀹告繈鏌? if (this.elements.remoteNetworkIndicator) { this.elements.remoteNetworkIndicator.classList.remove('hidden'); } @@ -245,24 +253,24 @@ class UIRenderer { renderHeaderTitle() { if (this.elements.headerTitle) { const connectionId = store.getConnectionId() || ''; - // 未连接时不显示红框部分 - this.elements.headerTitle.textContent = `通话 (${connectionId})`; + // 闁哄牜浜g换娑㈠箳閵夛附顦у☉鎾崇У濡绮堥搹鐟邦劉婵℃妫濋崕鎾礆? + this.elements.headerTitle.textContent = getCallTitle(connectionId); } } - // 渲染通话时长 + // 婵炴挸寮堕悡瀣焻濮樺磭妯堥柡鍐ㄧ埣閺? renderCallDuration(duration) { if (this.elements.callDuration) { this.elements.callDuration.textContent = formatTime(duration); } } - // 渲染分辨率变化 + // 婵炴挸寮堕悡瀣礆閸℃岸鍝洪柣婊冩搐瑜板宕? renderResolutionChanged(resolution) { if (!resolution) return; - // 更新分辨率选项的选中状态 + // 闁哄洤鐡ㄩ弻濠囧礆閸℃岸鍝洪柣婊冩喘閳ь剙顦甸妴宥夋儍閸曨垪鍋撴径澶庡幀闁绘鍩栭埀? const options = document.querySelectorAll('.resolution-option'); options.forEach(btn => { const btnRes = btn.dataset.resolution; @@ -273,107 +281,64 @@ class UIRenderer { btn.classList.toggle('active', isActive); }); - // 更新当前分辨率文本 + // 闁哄洤鐡ㄩ弻濠呫亹閹惧啿顤呴柛鎺戞妞存悂鎮抽崶銊︾€柡? const currentResText = document.getElementById('currentResolutionText'); if (currentResText) { currentResText.textContent = `当前: ${resolution.label}`; } } - // 渲染远端视频 + // 婵炴挸寮堕悡瀣交濠婂拋浼傞悷娆忔椤? renderRemoteVideo(remoteUser) { - // 同步更新侧边栏用户列表 - this.renderUserList(this.stateManager.getState().session.localUser, remoteUser, this.stateManager.getState().participants); + this.renderUserList( + this.stateManager.getState().session.localUser, + remoteUser, + this.stateManager.getState().participants + ); - // 当远程视频关闭时显示占位符 if (this.elements.remoteVideoPlaceholder) { const shouldShowPlaceholder = !remoteUser.mediaState.video; + const placeholderText = getRemoteVideoPlaceholderText(!shouldShowPlaceholder); toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder); - // 当远程视频关闭时,隐藏视频元素本身,避免冻结画面透过占位符 if (this.elements.remoteVideo) { - if (shouldShowPlaceholder) { - this.elements.remoteVideo.style.opacity = '0'; - } else { - this.elements.remoteVideo.style.opacity = '1'; - } + this.elements.remoteVideo.style.opacity = shouldShowPlaceholder ? '0' : '1'; } - // 更新占位符文本内容 - 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 = '对方暂时关闭了视频'; - } + const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center'); + if (placeholderContent) { + const titleElement = placeholderContent.querySelector('p.text-white.text-lg.font-medium'); + const subtitleElement = placeholderContent.querySelector('p.text-sm.text-gray-400'); + + if (titleElement) { + titleElement.textContent = placeholderText.title; } - } 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 (subtitleElement) { + subtitleElement.textContent = placeholderText.subtitle; } } } - // 渲染网络状态 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; + const display = getNetworkQualityDisplay(networkQuality); + textElement.textContent = display.label; + iconElement.className = display.headerIconClass; } } } - // 渲染本地视频 renderLocalVideo(localUser, localStream) { if (this.elements.localVideoPlaceholder) { - // 当没有视频流或视频关闭时显示占位符 + // 鐟滅増鎸婚惀鍛村嫉婢跺寒娼掑Λ鐗堝灦缁侊箓骞嬮弽顒夋綊濡増鍨甸崣褔姊婚鐔割槯闁哄嫬澧介妵姘跺础閻樿京绉寸紒? const shouldShowPlaceholder = !localStream || !localUser.mediaState.video; toggleElement(this.elements.localVideoPlaceholder, shouldShowPlaceholder); } @@ -382,19 +347,19 @@ class UIRenderer { 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; // 本地视频静音,避免回声 + this.elements.localVideo.muted = true; // 闁哄牜鍓欏﹢瀵告喆閸℃侗鏆ラ梻鍫熺懇閻撳爼鏁嶅畝鍕級闁稿繐绉村ú鏍ㄧ珶? console.log('srcObject set successfully:', this.elements.localVideo.srcObject); - // 隐藏断开连接覆盖层 + // 闂傚懏鍔樺Λ宀勫棘椤撶偟纾婚弶鈺冨仦鐢鎲伴崱娆愮0閻? if (this.elements.disconnectedOverlay) { this.elements.disconnectedOverlay.classList.add('hidden'); } @@ -403,27 +368,27 @@ class UIRenderer { } } - // 渲染远程视频流 + // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤h泛霉? renderRemoteStream(stream, connectionId, isHost) { if (isHost && connectionId) { - // Host端: 渲染到 participant 视频网格 + // Host缂? 婵炴挸寮堕悡瀣礆?participant 閻熸瑥妫濋。鍓佺磾閹寸偟澹? this.renderParticipantStream(stream, connectionId); } else { - // Participant端: 渲染到单一远端视频(Host的画面) + // Participant缂? 婵炴挸寮堕悡瀣礆閺夊灝绀嬪☉鎾亾閺夆晜绮庨顒傛喆閸℃侗鏆ラ柨娑樻箽ost闁汇劌瀚弫楣冩椤喚绀? this.renderSingleRemoteStream(stream); } } - // 渲染Host端的多Participant视频网格 - // 每个participant tile显示该participant实际的远端视频流 + // 婵炴挸寮堕悡濠琽st缂佹棏鍨冲▓鎴炲緞濮濆崋rticipant閻熸瑥妫濋。鍓佺磾閹寸偟澹? + // 婵絽绻嬮柌娓沘rticipant tile闁哄嫬澧介妵姘辨嫚椤хΨrticipant閻庡湱鍋ゅ顖炴儍閸曨喚绠肩紒鏃戝灥椤锛愰幋鐐点偊 renderParticipantStream(stream, connectionId) { const grid = this.elements.participantGrid; if (!grid) return; - // 显示网格,隐藏单路远端视频 + // 闁哄嫬澧介妵姘辩磾閹寸偟澹愰柨娑樼焸濞堬綁鎸婅箛鎾崇閻犱警鍨电换娆戠博椤栨繍娼掑Λ? grid.classList.remove('hidden'); - // 查找或创建该 connectionId 的视频格子 + // 闁哄被鍎叉竟姗€骞嬮弽褍鐏$€点倝缂氶?connectionId 闁汇劌瀚~瀣紣閹寸偟澹愰悗? let tile = grid.querySelector(`[data-participant-id="${connectionId}"]`); if (!tile) { tile = document.createElement('div'); @@ -434,11 +399,11 @@ class UIRenderer { video.className = 'w-full h-full object-contain'; video.autoplay = true; video.playsinline = true; - video.muted = false; // 不静音,播放participant的音频 + video.muted = false; // 濞戞挸绉瑰銈夋缁涘湱绀夐柟缁㈠幗閺備垢articipant闁汇劌瀚伴悡鑸碉紣? video.id = `participantVideo_${connectionId}`; tile.appendChild(video); - // 参与者视频关闭时的占位背景(复用 remoteVideoPlaceholder 样式) + // 闁告瑥鍊风粭宀勬嚀閸涢偊娼掑Λ鐗堝灥閸櫻囨⒒椤撶喐顦ч柣銊ュ瀹曠増鎷呭鍫濆壒闁哄拋鍨界槐娆愬緞瀹ュ洦鏆?remoteVideoPlaceholder 闁哄秴鍢茬槐锟犳晬? const placeholder = document.createElement('div'); placeholder.className = 'participant-video-placeholder absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900/80 to-purple-900/80 hidden'; placeholder.innerHTML = ` @@ -451,7 +416,7 @@ class UIRenderer { `; tile.appendChild(placeholder); - // 参与者名称标签(优先使用participants中的真实姓名) + // 闁告瑥鍊风粭宀勬嚀閸涱厽鍊崇紒澶屽閻栵絿绮甸幘鍛濞村吋锚閸樻稒鎷呯捄銊︽殢participants濞戞搩鍘惧▓鎴︽儑閻旈鏉藉┑顔芥尭閹洟鏁? const pInfo = this.stateManager.getState().participants[connectionId]; const displayName = pInfo?.name || '参与者'; const label = document.createElement('div'); @@ -459,7 +424,7 @@ class UIRenderer { label.innerHTML = `${displayName}`; tile.appendChild(label); - // 在线标识 + // 闁革负鍔庨崵搴ㄥ冀閸ヮ亞妲? const liveTag = document.createElement('div'); liveTag.className = 'absolute top-3 right-3 bg-green-500/80 px-2 py-0.5 rounded-full text-xs flex items-center gap-1'; liveTag.innerHTML = `在线`; @@ -470,10 +435,10 @@ class UIRenderer { } if (tile) { - // 视频元素显示participant的远端视频流 + // 閻熸瑥妫濋。鍫曞礂閸愵亞顦遍柡鍕⒔閵囨articipant闁汇劌瀚换娆戠博椤栨繍娼掑Λ鐗堝灦缁? const video = tile.querySelector('video'); if (video && stream) { - // 避免重复设置同一流对象(音频先到、视频后到时流对象相同) + // 闂侇剙鐏濋崢銈夋煂瀹ュ拋妲婚悹浣稿⒔閻ゅ棝宕ョ仦鑲╊伇婵炵繝绀侀顔炬寬閳藉懐绀勯梻濠冨▕椤e爼宕楅崼婵嗙厒闁靛棔娴囬~瀣紣閹存繃鍊甸柛鎺斿濡炲倸霉娴e壊鍤犻悹鐑囩磿濞村宕ュ畝瀣 if (video.srcObject === stream) { console.log(`Same stream for participant ${connectionId}, ensuring playback`); video.play().catch(e => console.log('Auto-play prevented:', e.message)); @@ -484,31 +449,31 @@ class UIRenderer { } } - // 隐藏单路远端视频和占位符 + // 闂傚懏鍔樺Λ宀勫础閺囷紕鐔呴弶鈺傜矌椤忣剛鎲撮崱娑辨殽闁告粌鑻畷鐗堟媴瀹ュ浂鍎? const remoteVideoDiv = this.elements.remoteVideo?.closest('.absolute.inset-0.video-fade-in'); if (remoteVideoDiv) { remoteVideoDiv.classList.add('hidden'); } } - // 根据参与者数量调整网格列数 + // 闁哄秷顫夊畵渚€宕i崒娆戠憿闁兼澘鎳忛弳鐔兼煂韫囨氨娈堕柡浣割嚟缂嶅寮介悡搴$仚闁? const tileCount = grid.querySelectorAll('[data-participant-id]').length; grid.style.gridTemplateColumns = getGridTemplateColumns(tileCount); - // 隐藏连接中提示 + // 闂傚懏鍔樺Λ灞炬交閻愭潙澶嶅☉鎿冨幗瑜颁胶绮? if (this.elements.connectingOverlay) { this.elements.connectingOverlay.classList.add('hidden'); } - // 隐藏远端视频占位符 + // 闂傚懏鍔樺Λ灞炬交濠婂拋浼傞悷娆忔椤e爼宕¢悩杈╃Т缂? if (this.elements.remoteVideoPlaceholder) { this.elements.remoteVideoPlaceholder.classList.add('hidden'); } } - // 精准更新指定participant tile的占位背景 - // participantId: 发送media-state-changed的participant的连接ID - // showPlaceholder: 是否显示占位背景(视频关闭时为true) + // 缂侇喗鍎抽崳顖炲即鐎涙ɑ鐓€闁圭娲ら悾绶媋rticipant tile闁汇劌瀚畷鐗堟媴瀹ュ牆鍓归柡? + // participantId: 闁告瑦鍨块埀顑挎矡edia-state-changed闁汇劌鍩rticipant闁汇劌瀚换娑㈠箳椤у粚 + // showPlaceholder: 闁哄嫷鍨伴幆渚€寮伴崜褋浠涢柛妤冨С缂嶅懘鎳楃仦鐐彲闁挎稑鐗愰~瀣紣閹存繂褰犻梻鍌ゅ幗濡炲倹绋夌花宄硊e闁? renderParticipantVideoPlaceholder(participantId, showPlaceholder) { const grid = this.elements.participantGrid; if (!grid) return; @@ -521,7 +486,7 @@ class UIRenderer { } } - // 同步更新所有participant tile的名称标签 + // 闁告艾鏈鐐哄即鐎涙ɑ鐓€闁圭鍋撻柡鍫濄€宎rticipant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒? syncParticipantTileNames(participants) { if (!participants) return; const grid = this.elements.participantGrid; @@ -531,7 +496,7 @@ class UIRenderer { } } - // 更新指定participant tile的名称标签 + // 闁哄洤鐡ㄩ弻濠囧箰閸パ呮毎participant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒? updateParticipantTileName(participantId, name) { const grid = this.elements.participantGrid; if (!grid) return; @@ -547,7 +512,7 @@ class UIRenderer { } } - // 渲染Participant端的单一远端视频(Host画面) + // 婵炴挸寮堕悡濠竌rticipant缂佹棏鍨冲▓鎴﹀础閺囨氨顏遍弶鈺傜矌椤忣剛鎲撮崱娑辨殽闁挎稑婀歰st闁汇垹顭峰浼存晬? renderSingleRemoteStream(stream) { if (!this.elements.remoteVideo || !stream) { console.error('Either remoteVideo element or stream is missing'); @@ -556,38 +521,38 @@ class UIRenderer { console.log('Rendering remote stream:', stream, 'tracks:', stream.getTracks().map(t => `${t.kind}(${t.readyState})`)); - // 关键修复:避免 srcObject = null 的重置模式 - // 如果 srcObject 已经是同一个 stream 对象,说明是同一流的轨道更新(如音频先到,视频后到) - // 浏览器会自动识别新添加的轨道,无需重置 srcObject + // 闁稿繑濞婇弫顓熺┍椤旂⒈妲婚柨娑欏哺娴尖晠宕?srcObject = null 闁汇劌瀚伴崳鍝ョ磾椤斾茎浣割嚕? + // 濠碘€冲€归悘?srcObject 鐎规瓕灏欑划锟犲及椤栨碍鍊卞☉鎾亾濞?stream 閻庣數顢婇挅鍕晬瀹€鍐惧殯闁哄嫬瀛╁Σ鎼佸触鐏炶偐顏辨繛缈犺兌濞堟垶娼妸鈺€澹曢柡鍥х摠閺屽﹪鏁嶉崼婵愭搐闂傚﹥濞婇。鍫曞礂閸繂鐓傞柨娑樼焷椤锛愰幋婵囧€甸柛鎺戝簻缁? + // 婵炴潙绻楅~宥夊闯閵娿倗绐楅柤濂変簻婵晝鎷犻崱妤€鐒奸柡鍌滃閸у﹪宕濋悩鍨暠閺夌偑鍔戞禍楣冩晬鐏炵偓锟ラ梻鍥e亾闂佹彃绉堕悿?srcObject if (this.elements.remoteVideo.srcObject === stream) { console.log('Same stream object, track added - ensuring playback'); this.elements.remoteVideo.play().catch(e => console.log('Auto-play prevented:', e.message)); return; } - // 首次设置或流对象变化:直接设置 srcObject(不使用 null 重置模式) + // 濡絾鐗楅鑲╂媼閸撗呮瀭闁瑰瓨鐗楃粊锔锯偓鐢殿攰閽栧嫰宕eΟ鍝勵嚙闁挎稒姘ㄥú鍧楀箳閵夘煈鍟庣紓?srcObject闁挎稑鐗呯粭澶嬫媴鐠恒劍鏆?null 闂佹彃绉堕悿鍡椢熼垾宕囩闁? this.elements.remoteVideo.srcObject = stream; this.elements.remoteVideo.autoplay = true; this.elements.remoteVideo.playsinline = true; this.elements.remoteVideo.muted = false; - // 确保视频开始播放 + // 缁绢収鍠曠换姘辨喆閸℃侗鏆ョ€殿喒鍋撳┑顔碱儐閹搁亶寮? this.elements.remoteVideo.play().catch(e => { console.log('Auto-play prevented, will retry on interaction:', e.message); }); - // 隐藏断开连接覆盖层 + // 闂傚懏鍔樺Λ宀勫棘椤撶偟纾婚弶鈺冨仦鐢鎲伴崱娆愮0閻? if (this.elements.disconnectedOverlay) { this.elements.disconnectedOverlay.classList.add('hidden'); } - // 监听视频轨道变化 + // 闁烩晜鍨甸幆澶屾喆閸℃侗鏆ラ弶鐐姂娴滈箖宕eΟ鍝勵嚙 const videoTracks = stream.getVideoTracks(); const audioTracks = stream.getAudioTracks(); console.log(`Stream has ${videoTracks.length} video tracks, ${audioTracks.length} audio tracks`); if (videoTracks.length > 0) { - // 有视频轨道:隐藏占位符 + // 闁哄牆顦抽~瀣紣閹达絽缂撻梺顒佹惈缁变即姊鹃幇顖涱棏闁告濮崇紞鍛箔? if (this.elements.remoteVideoPlaceholder) { this.elements.remoteVideoPlaceholder.classList.add('hidden'); } @@ -595,7 +560,7 @@ class UIRenderer { this.elements.connectingOverlay.classList.add('hidden'); } - // 监听视频轨道分辨率变化 + // 闁烩晜鍨甸幆澶屾喆閸℃侗鏆ラ弶鐐姂娴滈箖宕氶崱姘跺摵闁绘粌娲よぐ澶愬礌? const activeVideoTrack = videoTracks.find(track => track.readyState === 'live'); if (activeVideoTrack) { const resolution = this.getVideoResolution(activeVideoTrack); @@ -606,188 +571,66 @@ class UIRenderer { }); } } else { - // 只有音频轨道(视频轨道尚未到达):不显示占位符,等待视频轨道到达 - // 不设置 srcObject = null,保持音频播放 + // 闁告瑯浜濆﹢渚€妫呴幎绛嬫殽閺夌偑鍔戞禍楣冩晬閸綆娼掑Λ鐗堝灱瀵ゆ椽鏌嗛幘宕囨闁哄牜浜滈崺灞炬綇閹惧懐绀嗛柨娑欑煯缁楀寮伴崜褋浠涢柛妤冨С缂嶅懐绮敂鑲╃缂佹稑顦欢鐔烘喆閸℃侗鏆ラ弶鐐姂娴滈箖宕氶幏灞惧涧 + // 濞戞挸绉烽鏇犵磾?srcObject = null闁挎稑濂旂换姘跺箰娓氣偓閻撹埖锛愰幋鐐村啊闁衡偓? console.log('Audio-only stream, waiting for video track...'); } } - // 渲染本地用户状态 + // 婵炴挸寮堕悡瀣嫉椤掆偓濠€鎾偨閵婏箑鐓曢柣妯垮煐閳? renderLocalUserStatus(localUser) { - // 更新本地媒体状态文本 + const mediaMeta = getMediaStatusMeta(localUser.mediaState); + 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'; - } + this.elements.localMediaStatus.textContent = mediaMeta.text; + this.elements.localMediaStatus.className = mediaMeta.className; } - // 更新静音图标 if (this.elements.localMuteIcon) { - if (!localUser.mediaState.audio) { + if (mediaMeta.showMuteIcon) { this.elements.localMuteIcon.classList.remove('hidden'); - this.elements.localMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs'; + this.elements.localMuteIcon.className = mediaMeta.muteIconClass; } else { this.elements.localMuteIcon.classList.add('hidden'); } } } - // 渲染侧边栏用户列表(支持多Participant动态渲染) renderUserList(localUser, remoteUser, participants) { if (!this.elements.userList) return; const participantsMap = participants || {}; const participantCount = Object.keys(participantsMap).length; - - // 通话成员总数 = 本地用户(1) + participants中的条目数 - // Host端participants只含其他participant;Participant端participants含host+其他participant const userCount = 1 + participantCount; - // 更新通话成员总数显示 - const userCountElement = this.elements.userCountDisplay; - if (userCountElement) { - userCountElement.textContent = `通话成员 (${userCount})`; + if (this.elements.userCountDisplay) { + this.elements.userCountDisplay.textContent = buildUserCountLabel(userCount); } - // 清空列表并重新渲染 this.elements.userList.innerHTML = ''; - - // 1. 渲染本地用户 - // 判断当前用户角色:Host端localUser是主持人;Participant端localUser是参与者 - this.elements.userList.appendChild(this.createUserEntry({ + this.elements.userList.appendChild(createUserEntryElement({ user: localUser, role: 'local' })); - // 2. 渲染远端成员 if (participantCount > 0) { - // 有participants数据(Host端或Participant端收到participants-sync后) - for (const [pid, p] of Object.entries(participantsMap)) { - if (p.role === 'host') { - this.elements.userList.appendChild(this.createUserEntry({ - user: p, - role: 'host', - id: pid - })); - } else { - this.elements.userList.appendChild(this.createUserEntry({ - user: p, - role: 'participant', - id: pid - })); - } + for (const [participantId, participant] of Object.entries(participantsMap)) { + this.elements.userList.appendChild(createUserEntryElement({ + user: participant, + role: participant.role === 'host' ? 'host' : 'participant', + id: participantId + })); } } else if (remoteUser.status !== 'offline') { - // 兼容:Participant端未收到participants-sync时,使用remoteUser显示Host - this.elements.userList.appendChild(this.createUserEntry({ + this.elements.userList.appendChild(createUserEntryElement({ user: remoteUser, role: 'remote' })); } } - // 创建通用用户条目 - createUserEntry(options) { - const { user, role, id } = options; - - const div = document.createElement('div'); - const baseClass = 'flex items-center gap-3 p-2 rounded-lg'; - div.className = role === 'local' - ? `${baseClass} hover:bg-white/5` - : `${baseClass} bg-white/5`; - - // dataset.userId - switch (role) { - case 'local': - div.dataset.userId = 'local'; - break; - case 'remote': - div.dataset.userId = 'remote'; - break; - case 'host': - div.dataset.userId = `host_${id}`; - break; - case 'participant': - div.dataset.userId = `participant_${id}`; - break; - } - - const mediaState = user.mediaState; - const mediaStatusText = !mediaState.audio ? '静音中' : (!mediaState.video ? '视频关闭' : '在线'); - const mediaStatusClass = (!mediaState.audio || !mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400'; - const muteIconHtml = !mediaState.audio - ? '' - : ''; - - // 头像区域 - let avatarHtml; - if (role === 'local') { - avatarHtml = ``; - } else { - avatarHtml = ` -
- -
-
- `; - } - - // 角色标签 - let roleTag; - if (role === 'local') { - const isHost = user.isHost; - roleTag = isHost - ? '主持人' - : '参与者'; - } else if (role === 'participant') { - roleTag = '参与者'; - } else { - // remote, host - roleTag = '主持人'; - } - - // 媒体状态 data-field(仅local) - const dataFieldAttr = role === 'local' ? ' data-field="localUser.mediaStatus"' : ''; - - // 右侧内容 - let rightHtml; - if (role === 'participant') { - const speakingHtml = (mediaState.isSpeaking && mediaState.audio) - ? '
' - : ''; - rightHtml = ` -
- ${muteIconHtml} - ${speakingHtml} -
- `; - } else { - rightHtml = muteIconHtml; - } - - div.innerHTML = ` - ${avatarHtml} -
-
- ${user.name} - ${roleTag} -
-
${mediaStatusText}
-
- ${rightHtml} - `; - - return div; - } - // 在renderer.js中添加方法 - // 获取视频流分辨率 + // 闁革腹鏆渆nderer.js濞戞搩鍘介崸濠囧礉閻樿櫕鐓欐繛? + // 闁兼儳鍢茶ぐ鍥╂喆閸℃侗鏆ユ繛缈犵閸ㄥ孩娼忛妸褍鑺? getVideoResolution(track) { if (track && track.getSettings) { const settings = track.getSettings(); @@ -796,35 +639,35 @@ class UIRenderer { height: settings.height || 480 }; } - return { width: 640, 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布局,使视频居中 + // 閻犱礁澧介悿鍡欌偓鍦嚀濞呮帗绋夌弧绨俥x閻㈩垰鍟惇顒勬晬鐏炵厧鈻忛悷娆忔椤e墎浠﹂崨顒冨幀 container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.justifyContent = 'center'; - // 优化图像渲染 + // 濞村吋锚鐎垫煡宕堕幆褍鍓兼繛鎾冲级閻? videoElement.style.imageRendering = 'auto'; - // 确保视频元素在容器内正确显示 + // 缁绢収鍠曠换姘辨喆閸℃侗鏆ラ柛蹇撳暟缁€宀勫捶閵娿儺鍟囬柛锝冨妼閸炴潙顫㈤敐鍥b偓姗€寮伴崜褋浠? videoElement.style.maxWidth = '100%'; videoElement.style.maxHeight = '100%'; - videoElement.style.objectFit = 'contain'; // 保持原始比例,不裁剪 + videoElement.style.objectFit = 'contain'; // 濞e洦绻冪€垫棃宕㈤悢濂夋綏婵絾鏌х欢銉╂晬鐏炶偐鐟濋悷浣风婢光偓 } - // 渲染控制按钮 + // 婵炴挸寮堕悡瀣箳瑜嶉崺妤呭箰婢舵劖灏? renderControlButtons(mediaState) { if (this.elements.micBtn) { toggleButtonState(this.elements.micBtn, !mediaState.audio); @@ -839,35 +682,35 @@ class UIRenderer { } } - // 渲染聊天消息 + // 婵炴挸寮堕悡瀣嚂婵犲倶浜繛鎴濈墛娴? 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)}`; + 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'; @@ -882,7 +725,7 @@ class UIRenderer { let contentHTML = ''; if (message.type === 'file' && message.content.startsWith('data:image/')) { - // 图片消息 + // 闁搞儱澧芥晶鏍р槈閸喍绱? contentHTML = `
${message.fileName || '图片'} @@ -890,7 +733,7 @@ class UIRenderer {
`; } else { - // 文本消息 + // 闁哄倸娲﹀﹢鏉库槈閸喍绱? contentHTML = `
${message.content} @@ -914,7 +757,7 @@ class UIRenderer { return messageDiv; } - // 渲染未读消息数 + // 婵炴挸寮堕悡瀣嫉椤忓浂鍤㈡繛鎴濈墛娴煎懘寮? renderUnreadCount(count) { if (this.elements.unreadBadge) { if (count > 0) { @@ -926,7 +769,7 @@ class UIRenderer { } } - // 渲染侧边栏 + // 婵炴挸寮堕悡瀣瑹瑜戠粩鐔煎冀? renderSidebar(isOpen) { if (this.elements.sidebar) { if (isOpen) { @@ -937,119 +780,45 @@ class UIRenderer { } } - // 渲染网络状态 + // 婵炴挸寮堕悡瀣磾閹寸姷鎹曢柣妯垮煐閳? renderNetworkStatus(quality) { + const display = getNetworkQualityDisplay(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'; - } - - // 添加图标到网络状态元素 + icon.className = display.statusIconClass; + networkStatusText.textContent = display.label; + networkStatusText.className = display.statusTextClass; 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.elements.connectionQuality.textContent = `连接质量: ${display.label}`; + this.elements.connectionQuality.className = `text-xs ${display.connectionTextClass}`; } - // 同步更新头部网络指示器 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'; - } + this.elements.remoteNetworkIndicator.className = getNetworkQualityDisplay(networkQuality).indicatorClass; } - - // 渲染通话结束 renderCallEnded() { console.log('Call ended'); - // 清理participant网格 + // 婵炴挸鎳愰幃濡廰rticipant缂傚啯鍨堕悧? const grid = this.elements.participantGrid; if (grid) { grid.querySelectorAll('[data-participant-id]').forEach(tile => { @@ -1060,11 +829,11 @@ class UIRenderer { grid.classList.add('hidden'); } - // 跳转到结束通话界面 + // 閻犲搫鐤囧ù鍡涘礆閹殿喚娉㈤柡澶屽枛閳ь剚淇洪惁浠嬫偩瀹€鍕〃 window.location.href = './endcall/endcall.html'; } - // 渲染participant离开(host端,房间仍然存在) + // 婵炴挸寮堕悡濯漚rticipant缂佸倽顕х槐鎴︽晬閸ф攼st缂佹棏鍨界槐婵嬪箣閸ф锛熷ù鐘茬Ф閸斞呪偓娑櫭﹢顏堟晬? renderParticipantLeft(connectionId) { console.log(`Participant left: ${connectionId}, updating UI`); @@ -1098,29 +867,7 @@ class UIRenderer { } } - // 获取状态文本 - 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();