优化完成
This commit is contained in:
@@ -6,6 +6,25 @@ import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from '.
|
||||
import { mockCallSession } from './models.js';
|
||||
import chatMessage from './chatmessage.js';
|
||||
import store from './store.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;
|
||||
@@ -439,13 +458,7 @@ class UIRenderer {
|
||||
|
||||
// 根据参与者数量调整网格列数
|
||||
const tileCount = grid.querySelectorAll('[data-participant-id]').length;
|
||||
if (tileCount <= 1) {
|
||||
grid.style.gridTemplateColumns = '1fr';
|
||||
} else if (tileCount <= 4) {
|
||||
grid.style.gridTemplateColumns = 'repeat(2, 1fr)';
|
||||
} else {
|
||||
grid.style.gridTemplateColumns = 'repeat(3, 1fr)';
|
||||
}
|
||||
grid.style.gridTemplateColumns = getGridTemplateColumns(tileCount);
|
||||
|
||||
// 隐藏连接中提示
|
||||
if (this.elements.connectingOverlay) {
|
||||
@@ -613,146 +626,129 @@ class UIRenderer {
|
||||
|
||||
// 1. 渲染本地用户
|
||||
// 判断当前用户角色:Host端localUser是主持人;Participant端localUser是参与者
|
||||
this.elements.userList.appendChild(this.createLocalUserEntry(localUser));
|
||||
this.elements.userList.appendChild(this.createUserEntry({
|
||||
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.createHostEntry(pid, p));
|
||||
this.elements.userList.appendChild(this.createUserEntry({
|
||||
user: p,
|
||||
role: 'host',
|
||||
id: pid
|
||||
}));
|
||||
} else {
|
||||
this.elements.userList.appendChild(this.createParticipantEntry(pid, p));
|
||||
this.elements.userList.appendChild(this.createUserEntry({
|
||||
user: p,
|
||||
role: 'participant',
|
||||
id: pid
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else if (remoteUser.status !== 'offline') {
|
||||
// 兼容:Participant端未收到participants-sync时,使用remoteUser显示Host
|
||||
this.elements.userList.appendChild(this.createRemoteUserEntry(remoteUser));
|
||||
this.elements.userList.appendChild(this.createUserEntry({
|
||||
user: remoteUser,
|
||||
role: 'remote'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 创建本地用户条目
|
||||
createLocalUserEntry(localUser) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex items-center gap-3 p-2 rounded-lg hover:bg-white/5';
|
||||
div.dataset.userId = 'local';
|
||||
// 创建通用用户条目
|
||||
createUserEntry(options) {
|
||||
const { user, role, id } = options;
|
||||
|
||||
const mediaStatusText = !localUser.mediaState.audio ? '静音中' : (!localUser.mediaState.video ? '视频关闭' : '在线');
|
||||
const mediaStatusClass = (!localUser.mediaState.audio || !localUser.mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400';
|
||||
const muteIconHtml = !localUser.mediaState.audio
|
||||
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
|
||||
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
||||
: '';
|
||||
|
||||
// 根据是否为Host显示不同角色标签
|
||||
const isHost = localUser.isHost;
|
||||
const roleTag = isHost
|
||||
? '<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>'
|
||||
: '<span class="text-xs bg-purple-500 px-1.5 rounded ml-1">参与者</span>';
|
||||
// 头像区域
|
||||
let avatarHtml;
|
||||
if (role === 'local') {
|
||||
avatarHtml = `<img src="${user.avatar}" class="w-10 h-10 rounded-full object-cover">`;
|
||||
} else {
|
||||
avatarHtml = `
|
||||
<div class="relative">
|
||||
<img src="${user.avatar}" class="w-10 h-10 rounded-full object-cover">
|
||||
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 角色标签
|
||||
let roleTag;
|
||||
if (role === 'local') {
|
||||
const isHost = user.isHost;
|
||||
roleTag = isHost
|
||||
? '<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>'
|
||||
: '<span class="text-xs bg-purple-500 px-1.5 rounded ml-1">参与者</span>';
|
||||
} else if (role === 'participant') {
|
||||
roleTag = '<span class="text-xs bg-purple-500 px-1.5 rounded ml-1">参与者</span>';
|
||||
} else {
|
||||
// remote, host
|
||||
roleTag = '<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>';
|
||||
}
|
||||
|
||||
// 媒体状态 data-field(仅local)
|
||||
const dataFieldAttr = role === 'local' ? ' data-field="localUser.mediaStatus"' : '';
|
||||
|
||||
// 右侧内容
|
||||
let rightHtml;
|
||||
if (role === 'participant') {
|
||||
const speakingHtml = (mediaState.isSpeaking && mediaState.audio)
|
||||
? '<div class="audio-wave w-6"><span></span><span></span><span></span><span></span><span></span></div>'
|
||||
: '';
|
||||
rightHtml = `
|
||||
<div class="flex items-center gap-2">
|
||||
${muteIconHtml}
|
||||
${speakingHtml}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
rightHtml = muteIconHtml;
|
||||
}
|
||||
|
||||
div.innerHTML = `
|
||||
<img src="${localUser.avatar}" class="w-10 h-10 rounded-full object-cover">
|
||||
${avatarHtml}
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium">
|
||||
${localUser.name}
|
||||
${user.name}
|
||||
${roleTag}
|
||||
</div>
|
||||
<div class="${mediaStatusClass}" data-field="localUser.mediaStatus">${mediaStatusText}</div>
|
||||
<div class="${mediaStatusClass}"${dataFieldAttr}>${mediaStatusText}</div>
|
||||
</div>
|
||||
${muteIconHtml}
|
||||
${rightHtml}
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
// 创建Host条目(Participant端显示Host用)
|
||||
createHostEntry(hostId, hostInfo) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex items-center gap-3 p-2 rounded-lg bg-white/5';
|
||||
div.dataset.userId = `host_${hostId}`;
|
||||
|
||||
const mediaStatusText = !hostInfo.mediaState.audio ? '静音中' : (!hostInfo.mediaState.video ? '视频关闭' : '在线');
|
||||
const mediaStatusClass = (!hostInfo.mediaState.audio || !hostInfo.mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400';
|
||||
const muteIconHtml = !hostInfo.mediaState.audio
|
||||
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
||||
: '';
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="relative">
|
||||
<img src="${hostInfo.avatar}" class="w-10 h-10 rounded-full object-cover">
|
||||
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium">
|
||||
${hostInfo.name}
|
||||
<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>
|
||||
</div>
|
||||
<div class="${mediaStatusClass}">${mediaStatusText}</div>
|
||||
</div>
|
||||
${muteIconHtml}
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
// 创建远程用户条目(兼容回退:Participant端未收到participants-sync时显示Host)
|
||||
createRemoteUserEntry(remoteUser) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex items-center gap-3 p-2 rounded-lg bg-white/5';
|
||||
div.dataset.userId = 'remote';
|
||||
|
||||
const mediaStatusText = !remoteUser.mediaState.audio ? '静音中' : (!remoteUser.mediaState.video ? '视频关闭' : '在线');
|
||||
const mediaStatusClass = (!remoteUser.mediaState.audio || !remoteUser.mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400';
|
||||
const muteIconHtml = !remoteUser.mediaState.audio
|
||||
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
||||
: '';
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="relative">
|
||||
<img src="${remoteUser.avatar}" class="w-10 h-10 rounded-full object-cover">
|
||||
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium">
|
||||
${remoteUser.name}
|
||||
<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>
|
||||
</div>
|
||||
<div class="${mediaStatusClass}">${mediaStatusText}</div>
|
||||
</div>
|
||||
${muteIconHtml}
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
// 创建Participant条目(Host端显示每个Participant)
|
||||
createParticipantEntry(participantId, participant) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex items-center gap-3 p-2 rounded-lg bg-white/5';
|
||||
div.dataset.userId = `participant_${participantId}`;
|
||||
|
||||
const mediaStatusText = !participant.mediaState.audio ? '静音中' : (!participant.mediaState.video ? '视频关闭' : '在线');
|
||||
const mediaStatusClass = (!participant.mediaState.audio || !participant.mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400';
|
||||
const muteIconHtml = !participant.mediaState.audio
|
||||
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
||||
: '';
|
||||
const speakingHtml = (participant.mediaState.isSpeaking && participant.mediaState.audio)
|
||||
? '<div class="audio-wave w-6"><span></span><span></span><span></span><span></span><span></span></div>'
|
||||
: '';
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="relative">
|
||||
<img src="${participant.avatar}" class="w-10 h-10 rounded-full object-cover">
|
||||
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium">
|
||||
${participant.name}
|
||||
<span class="text-xs bg-purple-500 px-1.5 rounded ml-1">参与者</span>
|
||||
</div>
|
||||
<div class="${mediaStatusClass}">${mediaStatusText}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
${muteIconHtml}
|
||||
${speakingHtml}
|
||||
</div>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
// 在renderer.js中添加方法
|
||||
@@ -1058,13 +1054,7 @@ class UIRenderer {
|
||||
this.elements.remoteVideoPlaceholder.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
if (remainingTiles.length <= 1) {
|
||||
grid.style.gridTemplateColumns = '1fr';
|
||||
} else if (remainingTiles.length <= 4) {
|
||||
grid.style.gridTemplateColumns = 'repeat(2, 1fr)';
|
||||
} else {
|
||||
grid.style.gridTemplateColumns = 'repeat(3, 1fr)';
|
||||
}
|
||||
grid.style.gridTemplateColumns = getGridTemplateColumns(remainingTiles.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user