Files
video_socket-server/client/public/call/renderers/renderer-ui.js
2026-05-25 22:58:11 +08:00

226 lines
7.2 KiB
JavaScript

import { createIconElement, createTextElement, textValue } from '../../shared/dom.js';
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 getRoleTagMeta(user, role) {
if (role === 'local') {
return user.isHost
? { className: 'text-xs bg-indigo-500 px-1.5 rounded ml-1', label: '\u4e3b\u6301\u4eba' }
: { className: 'text-xs bg-purple-500 px-1.5 rounded ml-1', label: '\u53c2\u4e0e\u8005' };
}
if (role === 'participant') {
return { className: 'text-xs bg-purple-500 px-1.5 rounded ml-1', label: '\u53c2\u4e0e\u8005' };
}
return { className: 'text-xs bg-indigo-500 px-1.5 rounded ml-1', label: '\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 createAvatarImage(user) {
const image = document.createElement('img');
image.src = textValue(user.avatar);
image.alt = textValue(user.name, '\u7528\u6237');
image.className = 'w-10 h-10 rounded-full object-cover';
return image;
}
function createAvatarElement(user, role) {
if (role === 'local') {
return createAvatarImage(user);
}
const wrapper = document.createElement('div');
wrapper.className = 'relative';
wrapper.appendChild(createAvatarImage(user));
const statusDot = document.createElement('div');
statusDot.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900';
wrapper.appendChild(statusDot);
return wrapper;
}
function createAudioWaveElement() {
const wave = document.createElement('div');
wave.className = 'audio-wave w-6';
for (let i = 0; i < 5; i += 1) {
wave.appendChild(document.createElement('span'));
}
return wave;
}
function createRightElement(mediaState, role, muteIcon) {
if (role !== 'participant') {
return muteIcon;
}
const right = document.createElement('div');
right.className = 'flex items-center gap-2';
if (muteIcon) {
right.appendChild(muteIcon);
}
if (mediaState.isSpeaking && mediaState.audio) {
right.appendChild(createAudioWaveElement());
}
return right.childNodes.length > 0 ? right : null;
}
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 muteIcon = mediaMeta.showMuteIcon
? createIconElement(mediaMeta.muteIconClass)
: '';
const baseClass = 'flex items-center gap-3 p-2 rounded-lg';
entry.className = role === 'local'
? `${baseClass} hover:bg-white/5`
: `${baseClass} bg-white/5`;
entry.dataset.userId = getDatasetUserId(role, id);
entry.appendChild(createAvatarElement(user, role));
const details = document.createElement('div');
details.className = 'flex-1';
const nameRow = document.createElement('div');
nameRow.className = 'text-sm font-medium';
nameRow.appendChild(document.createTextNode(textValue(user.name)));
const roleTag = getRoleTagMeta(user, role);
nameRow.appendChild(createTextElement('span', roleTag.className, roleTag.label));
details.appendChild(nameRow);
const mediaStatus = createTextElement('div', mediaMeta.className, mediaMeta.text);
if (role === 'local') {
mediaStatus.dataset.field = 'localUser.mediaStatus';
}
details.appendChild(mediaStatus);
entry.appendChild(details);
const right = createRightElement(user.mediaState, role, muteIcon || null);
if (right) {
entry.appendChild(right);
}
return entry;
}