优化完成

This commit is contained in:
2026-04-25 21:09:45 +08:00
parent bcd55f9dac
commit d48ce78c03
10 changed files with 707 additions and 569 deletions

View File

@@ -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);
}
}