You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

971 lines
34 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 全局变量
let ws = null;
let username = null;
let currentChat = null;
let chatHistory = {}; // 存储每个用户的聊天记录 { username: [messages] }
let mediaRecorder = null;
let audioChunks = [];
let isRecording = false;
// 服务器配置 - 自动检测或使用配置的地址
const SERVER_CONFIG = {
// 云服务器公网IP
publicIP: '120.46.87.202',
port: '8080',
// 获取WebSocket地址
getWebSocketUrl: function() {
// 如果通过浏览器访问使用当前host
// 否则使用配置的公网IP
const host = window.location.host || `${this.publicIP}:${this.port}`;
return `ws://${host}`;
}
};
// 登录函数
function login() {
const usernameInput = document.getElementById('usernameInput');
username = usernameInput.value.trim();
if (!username) {
alert('请输入用户名');
usernameInput.focus();
return;
}
// 连接WebSocket
connectWebSocket();
}
// 连接WebSocket
function connectWebSocket() {
try {
// 构建WebSocket URL
const wsUrl = SERVER_CONFIG.getWebSocketUrl();
console.log('连接到:', wsUrl);
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket连接成功');
// 发送登录消息
const loginMsg = {
type: 'LOGIN',
sender: username
};
ws.send(JSON.stringify(loginMsg));
// 注意:不在这里切换界面,等待服务器确认
};
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
try {
const message = JSON.parse(event.data);
handleMessage(message);
} catch (e) {
console.error('解析消息失败:', e);
}
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
if (document.getElementById('loginModal').classList.contains('hidden')) {
// 已登录状态下的错误
console.error('连接错误,可能是消息过大或网络问题');
} else {
// 登录过程中的错误
alert('连接失败,请检查网络连接');
}
};
ws.onclose = (event) => {
console.log('WebSocket连接关闭, code:', event.code, 'reason:', event.reason);
if (!document.getElementById('chatContainer').classList.contains('hidden')) {
// 如果已经登录,显示断开提示
if (event.code === 1006) {
alert('连接异常断开可能是发送的文件过大。请尝试发送较小的文件建议小于5MB');
} else {
alert('连接已断开,请重新登录');
}
logout();
}
};
} catch (e) {
console.error('创建WebSocket失败:', e);
alert('连接失败: ' + e.message);
}
}
// 处理接收到的消息
function handleMessage(message) {
console.log('收到服务器消息:', message);
console.log('消息类型:', message.type);
switch (message.type) {
case 'LOGIN_SUCCESS':
console.log('登录成功');
document.getElementById('loginModal').classList.add('hidden');
document.getElementById('chatContainer').classList.remove('hidden');
document.getElementById('currentUser').textContent = username;
break;
case 'LOGIN_FAILED':
console.log('登录失败:', message.content);
alert(message.content);
// 重置状态,允许重新登录
ws.close();
ws = null;
username = null;
break;
case 'ACK':
console.log('服务器确认:', message.content);
break;
case 'USER_LIST':
updateUserList(message.users);
break;
case 'PRIVATE_MSG':
case 'FILE':
case 'IMAGE':
case 'VIDEO':
case 'AUDIO':
case 'VOICE':
console.log('收到多媒体消息:', message.type, '来自:', message.sender);
displayMessage(message, false);
break;
case 'ERROR':
alert('错误: ' + message.content);
break;
default:
console.log('未知消息类型:', message.type);
}
}
// 更新用户列表
function updateUserList(users) {
console.log('更新用户列表:', users);
const userList = document.getElementById('userList');
userList.innerHTML = '';
// 去重并过滤掉自己
const uniqueUsers = [...new Set(users)]; // 使用Set去重
const otherUsers = uniqueUsers.filter(u => u !== username && u); // 过滤掉自己和空值
console.log('去重后的其他用户:', otherUsers);
if (otherUsers.length === 0) {
userList.innerHTML = '<div class="empty-users">暂无其他用户在线</div>';
return;
}
otherUsers.forEach(user => {
const userItem = document.createElement('div');
userItem.className = 'user-item';
if (user === currentChat) {
userItem.classList.add('active');
}
// 计算未读消息数
const unreadCount = getUnreadCount(user);
const unreadBadge = unreadCount > 0 ? `<span class="unread-badge">${unreadCount}</span>` : '';
userItem.innerHTML = `
<span class="status"></span>
<span class="user-name">${escapeHtml(user)}</span>
${unreadBadge}
`;
userItem.onclick = () => selectUser(user);
userList.appendChild(userItem);
});
}
// 获取未读消息数量
function getUnreadCount(user) {
if (!chatHistory[user]) return 0;
const unreadCount = chatHistory[user].filter(msg => !msg.read && !msg.isSent).length;
console.log(`用户 ${user} 的未读消息数:`, unreadCount);
return unreadCount;
}
// 选择聊天用户
function selectUser(user) {
console.log('选择用户:', user);
currentChat = user;
// 更新标题
document.getElementById('chatTitle').textContent = `${user} 聊天`;
// 启用输入框
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const attachBtn = document.getElementById('attachBtn');
const voiceBtn = document.getElementById('voiceBtn');
messageInput.disabled = false;
sendBtn.disabled = false;
attachBtn.disabled = false;
voiceBtn.disabled = false;
messageInput.focus();
// 标记该用户的消息为已读
if (chatHistory[user]) {
const beforeCount = chatHistory[user].filter(msg => !msg.read && !msg.isSent).length;
chatHistory[user].forEach(msg => msg.read = true);
const afterCount = chatHistory[user].filter(msg => !msg.read && !msg.isSent).length;
console.log(`标记已读: ${user}, 之前未读: ${beforeCount}, 之后未读: ${afterCount}`);
}
// 更新用户列表选中状态和移除未读徽章
document.querySelectorAll('.user-item').forEach(item => {
const userName = item.querySelector('.user-name');
if (userName) {
const itemUser = userName.textContent.trim();
const isActive = itemUser === user;
item.classList.toggle('active', isActive);
// 如果是当前选中的用户,移除未读徽章
if (isActive) {
const badge = item.querySelector('.unread-badge');
if (badge) {
console.log(`移除 ${user} 的未读徽章`);
badge.remove();
}
}
}
});
// 加载该用户的聊天记录
loadChatHistory(user);
}
// 加载聊天记录
function loadChatHistory(user) {
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML = ''; // 清空当前显示
// 如果该用户没有聊天记录,初始化
if (!chatHistory[user]) {
chatHistory[user] = [];
}
// 如果有聊天记录,显示
if (chatHistory[user].length > 0) {
chatHistory[user].forEach(msg => {
displayMessageInDOM(msg);
});
} else {
// 没有聊天记录时显示提示
messagesDiv.innerHTML = `
<div class="welcome-screen">
<div class="welcome-icon">💬</div>
<p>开始与 ${user} 聊天吧</p>
</div>
`;
}
}
// 发送消息
function sendMessage() {
const messageInput = document.getElementById('messageInput');
const content = messageInput.value.trim();
if (!content) {
return;
}
if (!currentChat) {
alert('请先选择聊天对象');
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('连接已断开,请重新登录');
return;
}
console.log('发送消息:', content, '给:', currentChat);
// 发送消息到服务器
const message = {
type: 'PRIVATE_MSG',
sender: username,
receiver: currentChat,
content: content
};
ws.send(JSON.stringify(message));
// 显示发送的消息
displayMessage({
sender: username,
receiver: currentChat,
content: content,
timestamp: Date.now()
}, true);
// 清空输入框
messageInput.value = '';
messageInput.focus();
}
// 显示消息
// 显示消息
function displayMessage(message, isSent) {
console.log('displayMessage 被调用:', message, 'isSent:', isSent);
// 确保 timestamp 存在
if (!message.timestamp) {
message.timestamp = Date.now();
console.log('添加默认 timestamp:', message.timestamp);
}
// 确定这条消息属于哪个聊天对象
let chatUser;
if (isSent) {
chatUser = message.receiver;
} else {
chatUser = message.sender;
}
console.log('聊天对象:', chatUser, '当前聊天:', currentChat);
// 保存到聊天记录
if (!chatHistory[chatUser]) {
chatHistory[chatUser] = [];
}
chatHistory[chatUser].push({
type: message.type,
content: message.content,
fileName: message.fileName,
fileType: message.fileType,
fileSize: message.fileSize,
fileData: message.fileData,
duration: message.duration,
timestamp: message.timestamp,
isSent: isSent,
read: chatUser === currentChat // 如果是当前聊天对象,标记为已读
});
console.log('聊天记录已保存:', chatHistory[chatUser].length, '条消息');
// 只有当前聊天对象的消息才显示
if (chatUser === currentChat) {
console.log('显示消息到DOM');
displayMessageInDOM({
type: message.type,
content: message.content,
fileName: message.fileName,
fileType: message.fileType,
fileSize: message.fileSize,
fileData: message.fileData,
duration: message.duration,
timestamp: message.timestamp,
isSent: isSent
});
} else {
console.log('消息不属于当前聊天对象,不显示');
// 如果不是当前聊天对象,更新用户列表显示未读数
const userListItems = document.querySelectorAll('.user-item');
userListItems.forEach(item => {
const userName = item.querySelector('.user-name');
if (userName) {
const itemUser = userName.textContent.trim();
if (itemUser === chatUser) {
const unreadCount = getUnreadCount(chatUser);
let existingBadge = item.querySelector('.unread-badge');
if (unreadCount > 0) {
if (existingBadge) {
existingBadge.textContent = unreadCount;
} else {
const badge = document.createElement('span');
badge.className = 'unread-badge';
badge.textContent = unreadCount;
item.appendChild(badge);
}
} else if (existingBadge) {
existingBadge.remove();
}
}
}
});
}
}
// 在DOM中显示消息
function displayMessageInDOM(msg) {
console.log('displayMessageInDOM 被调用:', msg);
console.log('消息类型:', msg.type, '文件名:', msg.fileName, '时长:', msg.duration);
const messagesDiv = document.getElementById('messages');
// 移除欢迎屏幕(如果存在)
const welcomeScreen = messagesDiv.querySelector('.welcome-screen');
if (welcomeScreen) {
console.log('移除欢迎屏幕');
welcomeScreen.remove();
}
// 创建消息元素
const messageDiv = document.createElement('div');
messageDiv.className = `message ${msg.isSent ? 'sent' : 'received'}`;
const time = new Date(msg.timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
// 确定发送者名字
const senderName = msg.isSent ? username : currentChat;
console.log('发送者名字:', senderName);
let contentHtml = '';
// 根据消息类型生成不同的内容
if (msg.type === 'IMAGE') {
console.log('渲染图片消息');
const imageUrl = `data:${msg.fileType};base64,${msg.fileData}`;
contentHtml = `
<div class="message-file">
<img src="${imageUrl}" alt="${escapeHtml(msg.fileName)}" class="message-image" onclick="window.open(this.src)">
<div class="file-info">
<span class="file-name">${escapeHtml(msg.fileName)}</span>
<span class="file-size">${formatFileSize(msg.fileSize)}</span>
</div>
</div>
`;
} else if (msg.type === 'VIDEO') {
console.log('渲染视频消息');
const videoUrl = `data:${msg.fileType};base64,${msg.fileData}`;
contentHtml = `
<div class="message-file">
<video controls class="message-video">
<source src="${videoUrl}" type="${msg.fileType}">
您的浏览器不支持视频播放
</video>
<div class="file-info">
<span class="file-name">${escapeHtml(msg.fileName)}</span>
<span class="file-size">${formatFileSize(msg.fileSize)}</span>
</div>
</div>
`;
} else if (msg.type === 'AUDIO') {
console.log('渲染音频消息');
const audioUrl = `data:${msg.fileType};base64,${msg.fileData}`;
contentHtml = `
<div class="message-file">
<audio controls class="message-audio">
<source src="${audioUrl}" type="${msg.fileType}">
您的浏览器不支持音频播放
</audio>
<div class="file-info">
<span class="file-name">${escapeHtml(msg.fileName)}</span>
<span class="file-size">${formatFileSize(msg.fileSize)}</span>
</div>
</div>
`;
} else if (msg.type === 'VOICE') {
console.log('渲染语音消息, 时长:', msg.duration);
const audioUrl = `data:${msg.fileType};base64,${msg.fileData}`;
contentHtml = `
<div class="message-voice">
<span class="voice-icon">🎤</span>
<audio controls class="voice-audio">
<source src="${audioUrl}" type="${msg.fileType}">
您的浏览器不支持音频播放
</audio>
<span class="voice-duration">${msg.duration || 0}秒</span>
</div>
`;
} else if (msg.type === 'FILE') {
console.log('渲染文件消息');
contentHtml = `
<div class="message-file">
<div class="file-icon">📄</div>
<div class="file-info">
<span class="file-name">${escapeHtml(msg.fileName)}</span>
<span class="file-size">${formatFileSize(msg.fileSize)}</span>
</div>
<button class="download-btn" onclick='downloadFile("${escapeHtml(msg.fileName)}", "${msg.fileData}", "${msg.fileType}")'>下载</button>
</div>
`;
} else {
// 普通文本消息
console.log('渲染文本消息');
contentHtml = `<div class="message-text">${escapeHtml(msg.content)}</div>`;
}
messageDiv.innerHTML = `
<div class="message-wrapper">
<div class="message-sender">${escapeHtml(senderName)}</div>
<div class="message-content">
${contentHtml}
<div class="message-info">${time}</div>
</div>
</div>
`;
messagesDiv.appendChild(messageDiv);
console.log('消息已添加到DOM, 类型:', msg.type);
// 滚动到底部
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
// 退出登录
function logout() {
if (ws && ws.readyState === WebSocket.OPEN) {
const logoutMsg = {
type: 'LOGOUT',
sender: username
};
ws.send(JSON.stringify(logoutMsg));
ws.close();
}
// 重置状态
ws = null;
username = null;
currentChat = null;
chatHistory = {}; // 清空聊天记录
// 切换到登录界面
document.getElementById('chatContainer').classList.add('hidden');
document.getElementById('loginModal').classList.remove('hidden');
// 清空输入
document.getElementById('usernameInput').value = '';
document.getElementById('messageInput').value = '';
// 清空消息
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML = `
<div class="welcome-screen">
<div class="welcome-icon">💬</div>
<p>选择左侧用户开始聊天</p>
</div>
`;
}
// HTML转义函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 回车发送消息
document.addEventListener('DOMContentLoaded', () => {
const messageInput = document.getElementById('messageInput');
const usernameInput = document.getElementById('usernameInput');
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
login();
}
});
// 自动聚焦用户名输入框
usernameInput.focus();
// 显示连接信息
console.log('服务器地址:', SERVER_CONFIG.getWebSocketUrl());
// 检查浏览器功能支持
checkBrowserSupport();
});
// 检查浏览器功能支持
function checkBrowserSupport() {
const features = {
webSocket: typeof WebSocket !== 'undefined',
mediaDevices: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
mediaRecorder: typeof MediaRecorder !== 'undefined',
fileReader: typeof FileReader !== 'undefined'
};
console.log('浏览器功能支持:', features);
if (!features.webSocket) {
console.warn('浏览器不支持 WebSocket');
}
if (!features.mediaDevices) {
console.warn('浏览器不支持麦克风访问(需要 HTTPS 或 localhost');
const voiceBtn = document.getElementById('voiceBtn');
if (voiceBtn) {
voiceBtn.title = '您的浏览器不支持语音录制';
voiceBtn.style.opacity = '0.5';
}
}
if (!features.mediaRecorder) {
console.warn('浏览器不支持 MediaRecorder API');
}
// 检查是否在安全上下文中
const isSecureContext = window.isSecureContext ||
location.protocol === 'https:' ||
location.hostname === 'localhost' ||
location.hostname === '127.0.0.1';
if (!isSecureContext) {
console.warn('当前不在安全上下文中,某些功能可能无法使用');
console.warn('建议使用 HTTPS 或通过 localhost 访问');
}
console.log('安全上下文:', isSecureContext);
console.log('协议:', location.protocol);
console.log('主机:', location.hostname);
}
// 处理文件选择
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
if (!currentChat) {
alert('请先选择聊天对象');
return;
}
// 检查文件大小限制2MB以避免WebSocket断联
const maxSize = 2 * 1024 * 1024;
if (file.size > maxSize) {
alert('文件大小不能超过2MB\n' +
'当前文件大小: ' + formatFileSize(file.size) + '\n\n' +
'建议:\n' +
'1. 压缩图片/视频后再发送\n' +
'2. 使用 TCP 客户端发送大文件');
return;
}
console.log('准备发送文件:', file.name, '大小:', formatFileSize(file.size));
// 读取文件
const reader = new FileReader();
reader.onload = function(e) {
try {
const fileData = e.target.result;
const base64Data = fileData.split(',')[1]; // 移除data:xxx;base64,前缀
// 确定消息类型
let messageType = 'FILE';
if (file.type.startsWith('image/')) {
messageType = 'IMAGE';
} else if (file.type.startsWith('video/')) {
messageType = 'VIDEO';
} else if (file.type.startsWith('audio/')) {
messageType = 'AUDIO';
}
console.log('文件类型:', messageType, 'MIME:', file.type);
console.log('Base64 数据大小:', base64Data.length, '字符');
// 检查编码后的大小
const encodedSize = base64Data.length;
if (encodedSize > 100000) { // 100KB
alert('文件编码后过大,可能导致传输失败\n' +
'建议压缩文件或使用 TCP 客户端');
return;
}
// 发送文件消息
const message = {
type: messageType,
sender: username,
receiver: currentChat,
fileName: file.name,
fileType: file.type,
fileSize: file.size,
fileData: base64Data
};
console.log('发送文件消息');
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
console.log('文件消息已发送');
// 显示发送的文件消息
displayMessage({
type: messageType,
sender: username,
receiver: currentChat,
fileName: file.name,
fileType: file.type,
fileSize: file.size,
fileData: base64Data,
timestamp: Date.now()
}, true);
} else {
alert('连接已断开,请重新登录');
}
} catch (error) {
console.error('发送文件失败:', error);
alert('发送文件失败: ' + error.message);
}
};
reader.onerror = function(error) {
console.error('读取文件失败:', error);
alert('读取文件失败');
};
reader.readAsDataURL(file);
// 清空文件选择
event.target.value = '';
}
// 切换语音录制
async function toggleVoiceRecording() {
if (!currentChat) {
alert('请先选择聊天对象');
return;
}
const voiceBtn = document.getElementById('voiceBtn');
if (!isRecording) {
// 开始录音
try {
// 检查浏览器是否支持
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('您的浏览器不支持录音功能\n请使用 Chrome、Firefox 或 Edge 浏览器');
return;
}
// 检查是否支持 MediaRecorder
if (typeof MediaRecorder === 'undefined') {
alert('您的浏览器不支持 MediaRecorder API\n请更新浏览器到最新版本');
return;
}
console.log('请求麦克风权限...');
// 请求麦克风权限
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
console.log('麦克风权限已授予');
// 检查支持的 MIME 类型
let mimeType = 'audio/webm';
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
mimeType = 'audio/webm;codecs=opus';
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
mimeType = 'audio/webm';
} else if (MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')) {
mimeType = 'audio/ogg;codecs=opus';
} else if (MediaRecorder.isTypeSupported('audio/mp4')) {
mimeType = 'audio/mp4';
}
console.log('使用 MIME 类型:', mimeType);
mediaRecorder = new MediaRecorder(stream, { mimeType: mimeType });
audioChunks = [];
const startTime = Date.now();
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
console.log('录音数据块:', event.data.size, '字节');
}
};
mediaRecorder.onstop = async () => {
const duration = Math.floor((Date.now() - startTime) / 1000);
console.log('录音结束,时长:', duration, '秒');
if (audioChunks.length === 0) {
alert('录音失败:没有录制到音频数据');
stream.getTracks().forEach(track => track.stop());
return;
}
const audioBlob = new Blob(audioChunks, { type: mimeType });
console.log('音频大小:', formatFileSize(audioBlob.size));
// 检查文件大小限制2MB
if (audioBlob.size > 2 * 1024 * 1024) {
alert('语音消息过大超过2MB请录制较短的语音');
stream.getTracks().forEach(track => track.stop());
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const fileData = e.target.result;
const base64Data = fileData.split(',')[1];
// 确定文件扩展名
let extension = 'webm';
if (mimeType.includes('ogg')) {
extension = 'ogg';
} else if (mimeType.includes('mp4')) {
extension = 'm4a';
}
// 发送语音消息
const message = {
type: 'VOICE',
sender: username,
receiver: currentChat,
fileName: `voice_${Date.now()}.${extension}`,
fileType: mimeType,
fileSize: audioBlob.size,
fileData: base64Data,
duration: duration
};
console.log('发送语音消息');
ws.send(JSON.stringify(message));
// 显示发送的语音消息
displayMessage({
type: 'VOICE',
sender: username,
receiver: currentChat,
fileName: message.fileName,
fileType: message.fileType,
fileSize: audioBlob.size,
fileData: base64Data,
duration: duration,
timestamp: Date.now()
}, true);
};
reader.onerror = function(error) {
console.error('读取音频失败:', error);
alert('处理语音消息失败');
};
reader.readAsDataURL(audioBlob);
// 停止所有音轨
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.onerror = (event) => {
console.error('MediaRecorder 错误:', event.error);
alert('录音出错: ' + event.error.name);
isRecording = false;
voiceBtn.textContent = '🎤';
voiceBtn.style.backgroundColor = '';
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start(100); // 每100ms收集一次数据
isRecording = true;
voiceBtn.textContent = '⏹️';
voiceBtn.style.backgroundColor = '#f44336';
voiceBtn.title = '点击停止录音';
console.log('开始录音...');
} catch (error) {
console.error('无法访问麦克风:', error);
let errorMessage = '无法访问麦克风\n\n';
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
errorMessage += '原因:您拒绝了麦克风权限\n\n';
errorMessage += '解决方法:\n';
errorMessage += '1. 点击地址栏左侧的锁图标\n';
errorMessage += '2. 找到"麦克风"权限\n';
errorMessage += '3. 选择"允许"\n';
errorMessage += '4. 刷新页面重试';
} else if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') {
errorMessage += '原因:未检测到麦克风设备\n\n';
errorMessage += '解决方法:\n';
errorMessage += '1. 检查麦克风是否已连接\n';
errorMessage += '2. 检查系统声音设置\n';
errorMessage += '3. 确保麦克风未被其他程序占用';
} else if (error.name === 'NotReadableError' || error.name === 'TrackStartError') {
errorMessage += '原因:麦克风被其他程序占用\n\n';
errorMessage += '解决方法:\n';
errorMessage += '1. 关闭其他使用麦克风的程序\n';
errorMessage += '2. 重启浏览器\n';
errorMessage += '3. 检查系统麦克风设置';
} else if (error.name === 'OverconstrainedError') {
errorMessage += '原因:麦克风不支持请求的参数\n\n';
errorMessage += '解决方法:\n';
errorMessage += '1. 尝试使用其他麦克风\n';
errorMessage += '2. 更新音频驱动程序';
} else if (error.name === 'SecurityError') {
errorMessage += '原因:安全限制\n\n';
errorMessage += '解决方法:\n';
errorMessage += '1. 确保使用 HTTPS 或 localhost\n';
errorMessage += '2. 检查浏览器安全设置';
} else {
errorMessage += '错误详情:' + error.name + '\n';
errorMessage += error.message;
}
alert(errorMessage);
}
} else {
// 停止录音
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
console.log('停止录音');
mediaRecorder.stop();
isRecording = false;
voiceBtn.textContent = '🎤';
voiceBtn.style.backgroundColor = '';
voiceBtn.title = '语音消息';
}
}
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
// 下载文件
function downloadFile(fileName, fileData, fileType) {
const byteCharacters = atob(fileData);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: fileType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}