Files
video_socket-server/client/public/connectview.js
2026-05-18 21:12:05 +08:00

487 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* connect视图逻辑
* 处理初始连接界面的UI、用户设置、WebSocket连接状态显示
*/
import { showNotification } from './utils.js';
import store from './store.js';
const MAX_AVATAR_SIZE = 2 * 1024 * 1024; // 2MB
// WebSocket连接状态更新回调
let onWsStatusChange = null;
let cachedOnlineUsers = [];
/**
* 设置WebSocket状态变化回调
* @param {function} callback - 回调函数(connected: boolean)
*/
export function setWsStatusCallback(callback) {
onWsStatusChange = callback;
}
/**
* 更新WebSocket状态显示
* @param {boolean} connected - 是否已连接
*/
export function updateWsStatus(connected) {
const wsStatusDot = document.getElementById('wsStatusDot');
const wsStatusText = document.getElementById('wsStatusText');
if (wsStatusDot && wsStatusText) {
if (connected) {
wsStatusDot.className = 'w-2 h-2 bg-green-500 rounded-full animate-pulse';
wsStatusText.textContent = 'WebSocket已连接';
} else {
wsStatusDot.className = 'w-2 h-2 bg-gray-500 rounded-full';
wsStatusText.textContent = '未连接';
}
}
if (onWsStatusChange) {
onWsStatusChange(connected);
}
}
/**
* 初始化WebSocket连接页面加载时调用
*/
export async function initWebSocket() {
try {
await store.connectSignaling();
store.syncSocketUserInfo();
updateWsStatus(true);
await refreshOnlineUsers();
console.log('WebSocket initialized from connectview');
} catch (error) {
console.error('Failed to initialize WebSocket:', error);
updateWsStatus(false);
showNotification('WebSocket连接失败请刷新页面重试', 'error');
}
}
/**
* 获取全部在线WebSocket用户
* @param {boolean} silent - 是否静默刷新
*/
async function refreshOnlineUsers(silent = true) {
try {
const response = await fetch('/signaling/users');
if (!response.ok) {
throw new Error('Failed to fetch online users');
}
const data = await response.json();
cachedOnlineUsers = Array.isArray(data.users) ? data.users : [];
displayOnlineUsers(cachedOnlineUsers);
if (!silent) {
showNotification(`当前共有 ${cachedOnlineUsers.length} 个WebSocket用户在线`);
}
} catch (error) {
console.error('Error fetching online users:', error);
if (!silent) {
showNotification('获取在线用户失败', 'error');
}
}
}
/**
* 获取所有连接ID
*/
async function getAllConnectionIds() {
showNotification('正在获取连接ID和在线用户...');
try {
const [connectionResponse, usersResponse] = await Promise.all([
fetch('/signaling/connection-ids'),
fetch('/signaling/users')
]);
if (!connectionResponse.ok) {
throw new Error('Failed to fetch connection IDs');
}
if (!usersResponse.ok) {
throw new Error('Failed to fetch online users');
}
const connectionData = await connectionResponse.json();
const usersData = await usersResponse.json();
cachedOnlineUsers = Array.isArray(usersData.users) ? usersData.users : [];
displayConnectionIds(connectionData.connectionIds || []);
displayOnlineUsers(cachedOnlineUsers);
} catch (error) {
console.error('Error fetching connection IDs:', error);
showNotification('获取连接信息失败', 'error');
}
}
/**
* 显示连接ID列表
* @param {string[]} connectionIds - 连接ID数组
*/
function displayConnectionIds(connectionIds) {
const idsContainer = document.getElementById('idsContainer');
const connectionIdsList = document.getElementById('connectionIdsList');
if (idsContainer) {
idsContainer.innerHTML = '';
if (connectionIds.length === 0) {
idsContainer.innerHTML = '<p class="text-gray-500 text-sm">暂无可用的连接ID</p>';
} else {
connectionIds.forEach(id => {
const idElement = document.createElement('div');
idElement.className = 'flex items-center justify-between p-2 bg-white/5 rounded-lg hover:bg-white/10 cursor-pointer transition-colors';
idElement.innerHTML = `
<span class="text-sm">${id}</span>
<button class="text-xs bg-indigo-600 hover:bg-indigo-700 px-2 py-1 rounded" onclick="selectConnectionId('${id}')">选择</button>
`;
idsContainer.appendChild(idElement);
});
}
if (connectionIdsList) {
connectionIdsList.classList.remove('hidden');
}
showNotification(`找到 ${connectionIds.length} 个连接ID`);
}
}
/**
* 转义HTML特殊字符
* @param {string} value - 原始字符串
* @returns {string} 安全字符串
*/
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
* 显示全部在线WebSocket用户
* @param {Array} users - 在线用户列表
*/
function displayOnlineUsers(users) {
const onlineUsersList = document.getElementById('onlineUsersList');
const usersContainer = document.getElementById('usersContainer');
const onlineUsersSummary = document.getElementById('onlineUsersSummary');
if (!onlineUsersList || !usersContainer || !onlineUsersSummary) {
return;
}
onlineUsersSummary.textContent = `${users.length} 个WebSocket用户在线`;
usersContainer.innerHTML = '';
if (users.length === 0) {
usersContainer.innerHTML = '<p class="text-gray-500 text-sm">暂无在线用户</p>';
onlineUsersList.classList.remove('hidden');
return;
}
const groupedUsers = users.reduce((groups, user) => {
const groupName = user.connectionId ? `房间 ${user.connectionId}` : '大厅(未加入房间)';
if (!groups[groupName]) {
groups[groupName] = [];
}
groups[groupName].push(user);
return groups;
}, {});
Object.entries(groupedUsers).forEach(([groupName, roomUsers]) => {
const section = document.createElement('div');
section.className = 'rounded-lg border border-white/10 bg-white/5 p-3';
const roomTitle = document.createElement('div');
roomTitle.className = 'flex items-center justify-between mb-2';
roomTitle.innerHTML = `
<span class="text-sm font-medium text-white">${escapeHtml(groupName)}</span>
<span class="text-xs text-gray-400">${roomUsers.length} 人</span>
`;
section.appendChild(roomTitle);
const roomList = document.createElement('div');
roomList.className = 'space-y-2';
roomUsers.forEach((user) => {
const userName = user.name || user.userId || '匿名用户';
const avatar = user.avatar || '/images/p2.png';
const roleLabel = user.role === 'host' ? '房主' : (user.role === 'participant' ? '成员' : '大厅');
const userItem = document.createElement('div');
userItem.className = 'flex items-center justify-between rounded-lg bg-black/20 px-3 py-2';
userItem.innerHTML = `
<div class="flex items-center gap-3 min-w-0">
<img src="${escapeHtml(avatar)}" alt="${escapeHtml(userName)}" class="w-8 h-8 rounded-full object-cover">
<div class="min-w-0">
<div class="text-sm text-white truncate">${escapeHtml(userName)}</div>
<div class="text-xs text-gray-400 truncate">${escapeHtml(user.userId || user.socketId || user.participantId || '未设置ID')}</div>
</div>
</div>
<span class="text-xs px-2 py-1 rounded-full ${user.role === 'host' ? 'bg-indigo-500/20 text-indigo-300' : (user.role === 'participant' ? 'bg-white/10 text-gray-300' : 'bg-emerald-500/20 text-emerald-300')}">${roleLabel}</span>
`;
roomList.appendChild(userItem);
});
section.appendChild(roomList);
usersContainer.appendChild(section);
});
onlineUsersList.classList.remove('hidden');
}
/**
* 选择连接ID
* @param {string} id - 连接ID
*/
function selectConnectionId(id) {
const connectionIdInput = document.getElementById('connectionIdInput');
if (connectionIdInput) {
connectionIdInput.value = id;
showNotification(`已选择连接ID: ${id}`);
}
}
/**
* 生成8位用户ID
* @returns {string} 用户ID
*/
function generateUserId() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = 'user_';
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 加载用户设置
*/
export function loadUserSettings() {
const defaultAvatar = '/images/p1.png';
const userSettings = localStorage.getItem('userSettings');
if (userSettings) {
try {
const settings = JSON.parse(userSettings);
if (settings.userId) {
const userIdInput = document.getElementById('userIdInput');
if (userIdInput) userIdInput.value = settings.userId;
}
if (settings.name) {
const nicknameInput = document.getElementById('nicknameInput');
const userName = document.getElementById('userName');
if (nicknameInput) nicknameInput.value = settings.name;
if (userName) userName.textContent = settings.name;
}
const avatar = settings.avatar || defaultAvatar;
const userAvatar = document.getElementById('userAvatar');
const avatarPreview = document.getElementById('avatarPreview');
if (userAvatar) userAvatar.src = avatar;
if (avatarPreview) avatarPreview.src = avatar;
} catch (error) {
console.error('Error loading user settings:', error);
const userAvatar = document.getElementById('userAvatar');
const avatarPreview = document.getElementById('avatarPreview');
if (userAvatar) userAvatar.src = defaultAvatar;
if (avatarPreview) avatarPreview.src = defaultAvatar;
}
} else {
const newUserId = generateUserId();
const userIdInput = document.getElementById('userIdInput');
if (userIdInput) userIdInput.value = newUserId;
const userAvatar = document.getElementById('userAvatar');
const avatarPreview = document.getElementById('avatarPreview');
if (userAvatar) userAvatar.src = defaultAvatar;
if (avatarPreview) avatarPreview.src = defaultAvatar;
saveSettings();
}
}
/**
* 保存用户设置
*/
export function saveSettings() {
const defaultAvatar = '/images/p1.png';
const nicknameInput = document.getElementById('nicknameInput');
const userIdInput = document.getElementById('userIdInput');
const avatarPreview = document.getElementById('avatarPreview');
const userName = document.getElementById('userName');
const userAvatar = document.getElementById('userAvatar');
const settings = {
userId: userIdInput ? userIdInput.value : generateUserId(),
name: nicknameInput ? (nicknameInput.value || '我') : '我',
avatar: avatarPreview ? (avatarPreview.src || defaultAvatar) : defaultAvatar
};
localStorage.setItem('userSettings', JSON.stringify(settings));
store.syncSocketUserInfo(settings);
if (userName) userName.textContent = settings.name;
if (userAvatar) userAvatar.src = settings.avatar;
showNotification('设置已保存', 'success');
}
/**
* 处理头像上传
* @param {Event} event - 文件选择事件
*/
export function handleAvatarUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
showNotification('请选择图片文件', 'error');
return;
}
if (file.size > MAX_AVATAR_SIZE) {
showNotification('图片大小不能超过2MB', 'error');
return;
}
const formData = new FormData();
formData.append('avatar', file);
const userIdInput = document.getElementById('userIdInput');
if (userIdInput) {
formData.append('userId', userIdInput.value);
}
showNotification('正在上传头像...');
fetch('/api/upload/avatar', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('上传失败');
return response.json();
})
.then(data => {
if (data.success && data.avatarUrl) {
const avatarUrl = data.avatarUrl;
const avatarPreview = document.getElementById('avatarPreview');
const userAvatar = document.getElementById('userAvatar');
if (avatarPreview) avatarPreview.src = avatarUrl;
if (userAvatar) userAvatar.src = avatarUrl;
saveSettings();
showNotification('头像上传成功', 'success');
} else {
throw new Error('上传失败:' + (data.message || '未知错误'));
}
})
.catch(error => {
console.error('Error uploading avatar:', error);
showNotification('头像上传失败,请重试', 'error');
const defaultAvatar = '/images/p1.png';
const avatarPreview = document.getElementById('avatarPreview');
if (avatarPreview) avatarPreview.src = defaultAvatar;
});
}
/**
* 复制用户ID到剪贴板
*/
export function copyUserId() {
const userIdInput = document.getElementById('userIdInput');
if (userIdInput) {
userIdInput.select();
document.execCommand('copy');
showNotification('用户ID已复制到剪贴板', 'success');
}
}
/**
* 切换设置菜单
*/
export function toggleSettingsMenu() {
const settingsMenu = document.getElementById('settingsMenu');
if (settingsMenu) {
settingsMenu.classList.toggle('hidden');
}
}
/**
* 绑定connect视图事件
* @param {function} onJoinCall - 加入通话回调(connectionId: string)
* @param {function} onCreateCall - 创建通话回调()
*/
export function bindConnectViewEvents(onJoinCall, onCreateCall) {
// 加入通话按钮
const connectBtn = document.getElementById('connectBtn');
if (connectBtn) {
connectBtn.addEventListener('click', () => {
const connectionIdInput = document.getElementById('connectionIdInput');
const connectionId = connectionIdInput ? connectionIdInput.value.trim() : '';
if (connectionId) {
onJoinCall(connectionId);
} else {
showNotification('请输入连接ID', 'error');
}
});
}
// 创建通话按钮
const createCallBtn = document.getElementById('createCallBtn');
if (createCallBtn) {
createCallBtn.addEventListener('click', onCreateCall);
}
// 浏览全部ID按钮
const browseIdsBtn = document.getElementById('browseIdsBtn');
if (browseIdsBtn) {
browseIdsBtn.addEventListener('click', getAllConnectionIds);
}
// 输入框回车事件
const connectionIdInput = document.getElementById('connectionIdInput');
if (connectionIdInput) {
connectionIdInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const connectionId = connectionIdInput.value.trim();
if (connectionId) {
onJoinCall(connectionId);
} else {
showNotification('请输入连接ID', 'error');
}
}
});
}
// 用户设置按钮
const userSettingsBtn = document.getElementById('userSettingsBtn');
if (userSettingsBtn) {
userSettingsBtn.addEventListener('click', toggleSettingsMenu);
}
}
// 点击外部关闭设置菜单
document.addEventListener('click', function(event) {
const settingsMenu = document.getElementById('settingsMenu');
const userSettingsBtn = document.getElementById('userSettingsBtn');
if (settingsMenu && userSettingsBtn &&
!settingsMenu.contains(event.target) &&
!userSettingsBtn.contains(event.target)) {
settingsMenu.classList.add('hidden');
}
});
// 导出全局函数供HTML onclick使用
window.selectConnectionId = selectConnectionId;
window.saveSettings = saveSettings;
window.handleAvatarUpload = handleAvatarUpload;
window.copyUserId = copyUserId;
window.toggleSettingsMenu = toggleSettingsMenu;