/** * 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, breakpoints: [ { maxParticipants: 1, template: '1fr' }, { maxParticipants: 4, template: 'repeat(2, 1fr)' } ], defaultTemplate: 'repeat(3, 1fr)' }; function getGridTemplateColumns(participantCount) { for (const bp of GRID_LAYOUT.breakpoints) { if (participantCount <= bp.maxParticipants) { return bp.template; } } return GRID_LAYOUT.defaultTemplate; } 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'), // 濠㈣埖鐡峚rticipant閻熸瑥妫濋。鍓佺磾閹寸偟澹? participantGrid: document.getElementById('participantGrid'), // 濠㈣埖鎸抽崕鎾礃閸涱収鍟? 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'), 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', () => { 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; } } // 缂備焦鍨甸悾鐐鐎b晜顐介柣鈺傚灥閹宕? bindEventListeners() { // 濞存粌顑勫▎銏ゆ儎閹存繃鍎旈柛? } /** * 婵炴挸寮堕悡瀣棘鐟欏嫮銆?- 闁哄秷顫夊畵渚€鎮╅懜纰樺亾娴g缍侀柛鏍ㄧ墬濞插潡寮惂鐕? * @param {Object} state - 鐟滅増鎸告晶鐘虫償閺冨倹鏆忛柣妯垮煐閳? * @param {Object} changes - 闁绘鍩栭埀顑跨瑜板宕犻弽褜鍤犻悹? */ render(state, changes) { // 闁哄秷顫夊畵渚€宕eΟ鍝勵嚙缂侇偉顕ч悗鐑藉箥瑜戦、鎴炵▔瀹ュ懏鍊遍柣銊ュ鐟曞棝寮婚幘瀛樻儥濞? switch (changes.type) { case 'INIT': // 闁告帗绻傞~鎰板礌閺嶃劏顩柡?- 婵炴挸寮堕悡瀣箥閳ь剟寮垫繅姗ч柛蹇撳暟缁€? 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'); // 闁哄牆顦崇换娆戠矙鐎n偆銈﹂柡鍐ㄧ埣濞堬綁鎸婅箛鎾崇獥濞达絽绉烽崕妤呭疾? } else { this.elements.remoteVideoPlaceholder.classList.remove('hidden'); // 闁哄啰濮剧换娆戠矙鐎n偆銈﹂柡鍐煐濡绮堥崫鍕獥濞达絽绉烽崕妤呭疾? } } break; case 'DURATION_UPDATE': // 闂侇偅淇洪惁浠嬪籍閸洘姣愰柡鍥х摠閺?- 婵炴挸寮堕悡瀣焻濮樺磭妯堥柡鍐ㄧ埣閺? this.renderCallDuration(changes.duration); break; case 'LOCAL_MEDIA_CHANGE': // 闁哄牜鍓欏﹢瀛樺垔閹哄秶绉奸柣妯垮煐閳ь兛绀佽ぐ澶愬礌?- 闁哄洤鐡ㄩ弻濠囨儎缁嬪灝褰燯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': // 閺夆晜绮庨埢鐓幟规担钘夌闁告瑦鐗楅崹姘跺礉?- 闁哄洤鐡ㄩ弻濠冩交濠婂應鏌ら悷娆忔椤e爼寮伴崜褋浠? this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost); // 婵炴挸寮堕悡瀣交濠婂應鏌ゆ繛? // 鐟滅増鎹侀獮蹇涘矗閺嵮冪厒閺夆晜绮庨埢鐓幟规担瑙勵槯闁挎稑鐭傚▓锝夋寠韫囨氨绠鹃柟鎭掑劙閼垫垿骞撻幇顔轰粵 if (this.elements.connectingOverlay) { this.elements.connectingOverlay.classList.add('hidden'); } break; case 'REMOTE_MEDIA_CHANGE': // 閺夆晜绮庨埢鍏煎垔閹哄秶绉奸柣妯垮煐閳ь兛绀佽ぐ澶愬礌?- 闁哄洤鐡ㄩ弻濠冩交濠婂應鏌ら悷娆忔椤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) { // 濞寸姴绐媋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濞e洠鍓濇导鍛村矗濡搫顕?- 闂佹彃绉甸弻濠傘€掗崣澶屽帬闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧啴鐛捄鐑樺€辨慨婵愭ile闁告艾绉惰ⅷ this.renderUserList(state.session.localUser, state.session.remoteUser, changes.participants || state.participants); // 闁告艾鏈鐐哄即鐎涙ɑ鐓€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缂佸倽顕х槐?- 闁哄洤鐡ㄩ弻濂濞达絽妫欓崺褔姊荤紙鐘电煗闁绘帟娉涢悺銊╁捶? this.renderParticipantLeft(changes.connectionId); break; case 'RESOLUTION_CHANGED': // 闁告帒妫滄ご鎼佹偝閸パ冪秮闁?- 闁哄洤鐡ㄩ弻濂濞戞搩鍘惧▓鎴﹀礆閸℃岸鍝洪柣婊冩喘閳ь剙顦懙鎴︽偐閼哥鍋? this.renderResolutionChanged(changes.resolution); 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) { this.renderHeaderTitle(); 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); } renderHeaderTitle() { if (this.elements.headerTitle) { const connectionId = store.getConnectionId() || ''; // 闁哄牜浜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; const isActive = (resolution.height >= 1440 && btnRes === '1440') || (resolution.height >= 1080 && resolution.height < 1440 && btnRes === '1080') || (resolution.height >= 720 && resolution.height < 1080 && btnRes === '720') || (resolution.height < 720 && btnRes === '480'); 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 ); if (this.elements.remoteVideoPlaceholder) { const shouldShowPlaceholder = !remoteUser.mediaState.video; const placeholderText = getRemoteVideoPlaceholderText(!shouldShowPlaceholder); toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder); if (this.elements.remoteVideo) { this.elements.remoteVideo.style.opacity = shouldShowPlaceholder ? '0' : '1'; } 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; } if (subtitleElement) { subtitleElement.textContent = placeholderText.subtitle; } } } this.renderNetworkStatus(remoteUser.networkQuality); this.renderHeaderNetworkStatus(remoteUser.networkQuality); } renderHeaderNetworkStatus(networkQuality) { if (this.elements.remoteNetworkQuality) { const textElement = this.elements.remoteNetworkQuality.querySelector('span'); const iconElement = this.elements.remoteNetworkQuality.querySelector('i'); if (textElement && iconElement) { 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); } 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); // 闂傚懏鍔樺Λ宀勫棘椤撶偟纾婚弶鈺冨仦鐢鎲伴崱娆愮0閻? if (this.elements.disconnectedOverlay) { this.elements.disconnectedOverlay.classList.add('hidden'); } } else { console.error('Either localVideo element or stream is missing'); } } // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤h泛霉? renderRemoteStream(stream, connectionId, isHost) { if (isHost && connectionId) { // Host缂? 婵炴挸寮堕悡瀣礆?participant 閻熸瑥妫濋。鍓佺磾閹寸偟澹? this.renderParticipantStream(stream, connectionId); } else { // Participant缂? 婵炴挸寮堕悡瀣礆閺夊灝绀嬪☉鎾亾閺夆晜绮庨顒傛喆閸℃侗鏆ラ柨娑樻箽ost闁汇劌瀚弫楣冩椤喚绀? this.renderSingleRemoteStream(stream); } } // 婵炴挸寮堕悡濠琽st缂佹棏鍨冲▓鎴炲緞濮濆崋rticipant閻熸瑥妫濋。鍓佺磾閹寸偟澹? // 婵絽绻嬮柌娓沘rticipant tile闁哄嫬澧介妵姘辨嫚椤хΨrticipant閻庡湱鍋ゅ顖炴儍閸曨喚绠肩紒鏃戝灥椤锛愰幋鐐点偊 renderParticipantStream(stream, connectionId) { const grid = this.elements.participantGrid; if (!grid) return; // 闁哄嫬澧介妵姘辩磾閹寸偟澹愰柨娑樼焸濞堬綁鎸婅箛鎾崇閻犱警鍨电换娆戠博椤栨繍娼掑Λ? grid.classList.remove('hidden'); // 闁哄被鍎叉竟姗€骞嬮弽褍鐏$€点倝缂氶?connectionId 闁汇劌瀚~瀣紣閹寸偟澹愰悗? let tile = grid.querySelector(`[data-participant-id="${connectionId}"]`); if (!tile) { tile = document.createElement('div'); tile.className = 'relative bg-black/60 rounded-xl overflow-hidden flex items-center justify-center'; tile.dataset.participantId = connectionId; const video = document.createElement('video'); video.className = 'w-full h-full object-contain'; video.autoplay = true; video.playsinline = true; video.muted = false; // 濞戞挸绉瑰銈夋缁涘湱绀夐柟缁㈠幗閺備垢articipant闁汇劌瀚伴悡鑸碉紣? video.id = `participantVideo_${connectionId}`; tile.appendChild(video); // 闁告瑥鍊风粭宀勬嚀閸涢偊娼掑Λ鐗堝灥閸櫻囨⒒椤撶喐顦ч柣銊ュ瀹曠増鎷呭鍫濆壒闁哄拋鍨界槐娆愬緞瀹ュ洦鏆?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 = `
摄像头已关闭