2026-05-25 22:58:11 +08:00
|
|
|
import { createIconElement, createTextElement, textValue } from '../../shared/dom.js';
|
|
|
|
|
|
2026-05-24 13:29:54 +08:00
|
|
|
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'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-25 22:58:11 +08:00
|
|
|
function getRoleTagMeta(user, role) {
|
2026-05-24 13:29:54 +08:00
|
|
|
if (role === 'local') {
|
|
|
|
|
return user.isHost
|
2026-05-25 22:58:11 +08:00
|
|
|
? { 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' };
|
2026-05-24 13:29:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (role === 'participant') {
|
2026-05-25 22:58:11 +08:00
|
|
|
return { className: 'text-xs bg-purple-500 px-1.5 rounded ml-1', label: '\u53c2\u4e0e\u8005' };
|
2026-05-24 13:29:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 22:58:11 +08:00
|
|
|
return { className: 'text-xs bg-indigo-500 px-1.5 rounded ml-1', label: '\u4e3b\u6301\u4eba' };
|
2026-05-24 13:29:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 22:58:11 +08:00
|
|
|
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) {
|
2026-05-24 13:29:54 +08:00
|
|
|
if (role === 'local') {
|
2026-05-25 22:58:11 +08:00
|
|
|
return createAvatarImage(user);
|
2026-05-24 13:29:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 22:58:11 +08:00
|
|
|
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;
|
2026-05-24 13:29:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 22:58:11 +08:00
|
|
|
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) {
|
2026-05-24 13:29:54 +08:00
|
|
|
if (role !== 'participant') {
|
2026-05-25 22:58:11 +08:00
|
|
|
return muteIcon;
|
2026-05-24 13:29:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 22:58:11 +08:00
|
|
|
const right = document.createElement('div');
|
|
|
|
|
right.className = 'flex items-center gap-2';
|
2026-05-24 13:29:54 +08:00
|
|
|
|
2026-05-25 22:58:11 +08:00
|
|
|
if (muteIcon) {
|
|
|
|
|
right.appendChild(muteIcon);
|
|
|
|
|
}
|
|
|
|
|
if (mediaState.isSpeaking && mediaState.audio) {
|
|
|
|
|
right.appendChild(createAudioWaveElement());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return right.childNodes.length > 0 ? right : null;
|
2026-05-24 13:29:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-05-25 22:58:11 +08:00
|
|
|
const muteIcon = mediaMeta.showMuteIcon
|
|
|
|
|
? createIconElement(mediaMeta.muteIconClass)
|
2026-05-24 13:29:54 +08:00
|
|
|
: '';
|
|
|
|
|
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);
|
2026-05-25 22:58:11 +08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-05-24 13:29:54 +08:00
|
|
|
|
|
|
|
|
return entry;
|
|
|
|
|
}
|