|
|
|
|
@ -3,6 +3,9 @@ let ws = null;
|
|
|
|
|
let username = null;
|
|
|
|
|
let currentChat = null;
|
|
|
|
|
let chatHistory = {}; // 存储每个用户的聊天记录 { username: [messages] }
|
|
|
|
|
let mediaRecorder = null;
|
|
|
|
|
let audioChunks = [];
|
|
|
|
|
let isRecording = false;
|
|
|
|
|
|
|
|
|
|
// 服务器配置 - 自动检测或使用配置的地址
|
|
|
|
|
const SERVER_CONFIG = {
|
|
|
|
|
@ -121,6 +124,11 @@ function handleMessage(message) {
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'PRIVATE_MSG':
|
|
|
|
|
case 'FILE':
|
|
|
|
|
case 'IMAGE':
|
|
|
|
|
case 'VIDEO':
|
|
|
|
|
case 'AUDIO':
|
|
|
|
|
case 'VOICE':
|
|
|
|
|
displayMessage(message, false);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
@ -192,8 +200,12 @@ function selectUser(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();
|
|
|
|
|
|
|
|
|
|
// 标记该用户的消息为已读
|
|
|
|
|
@ -316,7 +328,13 @@ function displayMessage(message, isSent) {
|
|
|
|
|
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 // 如果是当前聊天对象,标记为已读
|
|
|
|
|
@ -328,7 +346,13 @@ function displayMessage(message, isSent) {
|
|
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
@ -388,11 +412,80 @@ function displayMessageInDOM(msg) {
|
|
|
|
|
const senderName = msg.isSent ? username : currentChat;
|
|
|
|
|
console.log('发送者名字:', senderName);
|
|
|
|
|
|
|
|
|
|
let contentHtml = '';
|
|
|
|
|
|
|
|
|
|
// 根据消息类型生成不同的内容
|
|
|
|
|
if (msg.type === 'IMAGE') {
|
|
|
|
|
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') {
|
|
|
|
|
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') {
|
|
|
|
|
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') {
|
|
|
|
|
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') {
|
|
|
|
|
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 {
|
|
|
|
|
// 普通文本消息
|
|
|
|
|
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">
|
|
|
|
|
<div class="message-text">${escapeHtml(msg.content)}</div>
|
|
|
|
|
${contentHtml}
|
|
|
|
|
<div class="message-info">${time}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -472,3 +565,176 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
// 显示连接信息
|
|
|
|
|
console.log('服务器地址:', SERVER_CONFIG.getWebSocketUrl());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 处理文件选择
|
|
|
|
|
function handleFileSelect(event) {
|
|
|
|
|
const file = event.target.files[0];
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
if (!currentChat) {
|
|
|
|
|
alert('请先选择聊天对象');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查文件大小(限制10MB)
|
|
|
|
|
const maxSize = 10 * 1024 * 1024;
|
|
|
|
|
if (file.size > maxSize) {
|
|
|
|
|
alert('文件大小不能超过10MB');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 读取文件
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = function(e) {
|
|
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发送文件消息
|
|
|
|
|
const message = {
|
|
|
|
|
type: messageType,
|
|
|
|
|
sender: username,
|
|
|
|
|
receiver: currentChat,
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
fileType: file.type,
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
fileData: base64Data
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.send(JSON.stringify(message));
|
|
|
|
|
|
|
|
|
|
// 显示发送的文件消息
|
|
|
|
|
displayMessage({
|
|
|
|
|
type: messageType,
|
|
|
|
|
sender: username,
|
|
|
|
|
receiver: currentChat,
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
fileType: file.type,
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
fileData: base64Data,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}, true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
|
|
|
|
// 清空文件选择
|
|
|
|
|
event.target.value = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 切换语音录制
|
|
|
|
|
async function toggleVoiceRecording() {
|
|
|
|
|
if (!currentChat) {
|
|
|
|
|
alert('请先选择聊天对象');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const voiceBtn = document.getElementById('voiceBtn');
|
|
|
|
|
|
|
|
|
|
if (!isRecording) {
|
|
|
|
|
// 开始录音
|
|
|
|
|
try {
|
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
|
|
|
mediaRecorder = new MediaRecorder(stream);
|
|
|
|
|
audioChunks = [];
|
|
|
|
|
|
|
|
|
|
mediaRecorder.ondataavailable = (event) => {
|
|
|
|
|
audioChunks.push(event.data);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mediaRecorder.onstop = async () => {
|
|
|
|
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
|
|
|
|
reader.onload = function(e) {
|
|
|
|
|
const fileData = e.target.result;
|
|
|
|
|
const base64Data = fileData.split(',')[1];
|
|
|
|
|
|
|
|
|
|
// 发送语音消息
|
|
|
|
|
const message = {
|
|
|
|
|
type: 'VOICE',
|
|
|
|
|
sender: username,
|
|
|
|
|
receiver: currentChat,
|
|
|
|
|
fileName: `voice_${Date.now()}.webm`,
|
|
|
|
|
fileType: 'audio/webm',
|
|
|
|
|
fileSize: audioBlob.size,
|
|
|
|
|
fileData: base64Data,
|
|
|
|
|
duration: Math.floor(audioChunks.length / 10) // 估算时长
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.send(JSON.stringify(message));
|
|
|
|
|
|
|
|
|
|
// 显示发送的语音消息
|
|
|
|
|
displayMessage({
|
|
|
|
|
type: 'VOICE',
|
|
|
|
|
sender: username,
|
|
|
|
|
receiver: currentChat,
|
|
|
|
|
fileName: message.fileName,
|
|
|
|
|
fileType: message.fileType,
|
|
|
|
|
fileSize: audioBlob.size,
|
|
|
|
|
fileData: base64Data,
|
|
|
|
|
duration: message.duration,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}, true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
reader.readAsDataURL(audioBlob);
|
|
|
|
|
|
|
|
|
|
// 停止所有音轨
|
|
|
|
|
stream.getTracks().forEach(track => track.stop());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mediaRecorder.start();
|
|
|
|
|
isRecording = true;
|
|
|
|
|
voiceBtn.textContent = '⏹️';
|
|
|
|
|
voiceBtn.style.backgroundColor = '#f44336';
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('无法访问麦克风:', error);
|
|
|
|
|
alert('无法访问麦克风,请检查权限设置');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 停止录音
|
|
|
|
|
mediaRecorder.stop();
|
|
|
|
|
isRecording = false;
|
|
|
|
|
voiceBtn.textContent = '🎤';
|
|
|
|
|
voiceBtn.style.backgroundColor = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化文件大小
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|