hnu202326010206 4 months ago
parent dca6647a0b
commit 29fa0a0972

@ -0,0 +1,218 @@
# 文件传输和多媒体功能使用指南
## 功能概述
本聊天系统现已支持以下多媒体功能:
### 1. 普通文件传输
- 支持任意类型的文件传输
- 文件大小限制10MB
- 自动保存到 `downloads` 目录
### 2. 图片传输与显示
- 支持格式JPG, PNG, GIF, BMP, WebP
- 自动在聊天界面中显示图片预览
- 点击图片可在新窗口查看原图
### 3. 视频传输与播放
- 支持格式MP4, AVI, MOV, WMV, FLV, WebM
- 内置视频播放器,支持在线播放
- 播放控制:播放/暂停、进度条、音量调节
### 4. 音频传输与播放
- 支持格式MP3, WAV, OGG, M4A, FLAC
- 内置音频播放器
- 播放控制:播放/暂停、进度条、音量调节
### 5. 语音聊天
- 实时录制语音消息
- 支持语音播放
- 显示语音时长
## 使用方法
### TCP客户端命令行
#### 发送文件
```bash
/file @用户名 文件路径
```
示例:
```bash
# 发送图片
/file @Alice photo.jpg
# 发送视频
/file @Bob video.mp4
# 发送音频
/file @Charlie music.mp3
# 发送普通文件
/file @David document.pdf
```
#### 接收文件
- 文件会自动保存到 `downloads` 目录
- 文件名格式:`用户名_原文件名`
- 控制台会显示文件信息和保存路径
### Web客户端浏览器
#### 发送文件
1. 点击输入框左侧的 📎 按钮
2. 选择要发送的文件
3. 文件会自动发送给当前聊天对象
#### 发送语音消息
1. 点击输入框右侧的 🎤 按钮开始录音
2. 按钮变为 ⏹️ 表示正在录音
3. 再次点击停止录音并发送
#### 查看多媒体内容
- **图片**:自动显示在聊天界面,点击可放大查看
- **视频**:显示视频播放器,点击播放按钮观看
- **音频**:显示音频播放器,点击播放按钮收听
- **语音**:显示语音播放控件,点击播放
- **普通文件**:显示文件信息,点击"下载"按钮保存
## 技术实现
### 消息类型
```java
public enum MessageType {
FILE, // 普通文件
IMAGE, // 图片文件
VIDEO, // 视频文件
AUDIO, // 音频文件
VOICE // 语音消息
}
```
### 消息结构
```java
Message {
MessageType type; // 消息类型
String sender; // 发送者
String receiver; // 接收者
String fileName; // 文件名
String fileType; // MIME类型
long fileSize; // 文件大小(字节)
byte[] fileData; // 文件数据
int duration; // 音视频时长(秒)
Date timestamp; // 时间戳
}
```
### 文件类型识别
系统根据文件扩展名自动识别文件类型:
- **图片**.jpg, .jpeg, .png, .gif, .bmp, .webp
- **视频**.mp4, .avi, .mov, .wmv, .flv, .webm
- **音频**.mp3, .wav, .ogg, .m4a, .flac
- **其他**:作为普通文件处理
## 注意事项
1. **文件大小限制**
- Web客户端最大10MB
- TCP客户端理论上无限制但建议不超过100MB
2. **浏览器兼容性**
- 语音录制需要浏览器支持 MediaRecorder API
- 建议使用 Chrome、Firefox、Edge 等现代浏览器
3. **网络传输**
- 大文件传输可能需要较长时间
- 建议在稳定的网络环境下使用
4. **存储空间**
- 接收的文件会保存在本地
- 定期清理 `downloads` 目录以释放空间
5. **安全性**
- 文件传输未加密,请勿传输敏感信息
- 接收文件前请确认发送者身份
## 编译和运行
### 编译
```bash
# Windows
compile.bat
# Linux/Mac
./compile.sh
```
### 运行服务器
```bash
# TCP服务器
run_server.bat # Windows
./run_server.sh # Linux/Mac
# WebSocket服务器支持Web客户端
run_web_server.bat # Windows
./run_web_server.sh # Linux/Mac
```
### 运行客户端
```bash
# TCP客户端
run_client.bat # Windows
./run_client.sh # Linux/Mac
# Web客户端
# 在浏览器中访问 http://localhost:8080
```
## 示例场景
### 场景1发送图片
```
用户A: /file @用户B photo.jpg
系统: 正在发送文件: photo.jpg (2.5 MB)
系统: 文件发送完成
用户B收到:
[用户A] 发送了图片: photo.jpg
文件大小: 2.5 MB
已保存到: downloads/用户B_photo.jpg
```
### 场景2发送语音消息
```
用户A: 点击🎤按钮录制5秒语音
系统: 语音消息已发送
用户B收到:
[用户A] 发送了语音消息 (5秒)
已保存到: downloads/用户B_voice_1234567890.webm
```
## 故障排除
### 问题1无法发送文件
- 检查文件是否存在
- 检查文件大小是否超过限制
- 确认已选择聊天对象
### 问题2无法录制语音
- 检查浏览器是否支持 MediaRecorder API
- 确认已授予麦克风权限
- 尝试刷新页面重新授权
### 问题3文件无法播放
- 检查浏览器是否支持该文件格式
- 尝试下载文件后使用本地播放器
- 确认文件未损坏
## 未来改进
- [ ] 支持文件分片传输(大文件)
- [ ] 添加传输进度显示
- [ ] 支持文件预览PDF、Office文档
- [ ] 添加文件加密传输
- [ ] 支持视频通话
- [ ] 添加屏幕共享功能

@ -4,6 +4,9 @@ import common.Message;
import common.MessageType;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;
/**
@ -18,11 +21,18 @@ public class ChatClient {
private String username;
private volatile boolean running;
private MessageReceiver receiver;
private static final String DOWNLOAD_DIR = "downloads";
public ChatClient(String serverHost, int serverPort) {
this.serverHost = serverHost;
this.serverPort = serverPort;
this.running = false;
// 创建下载目录
File downloadDir = new File(DOWNLOAD_DIR);
if (!downloadDir.exists()) {
downloadDir.mkdirs();
}
}
public boolean connect(String username) {
@ -61,6 +71,140 @@ public class ChatClient {
sendMessage(message);
}
/**
*
*/
public void sendFile(String receiver, String filePath) {
try {
File file = new File(filePath);
if (!file.exists()) {
System.out.println("文件不存在: " + filePath);
return;
}
// 读取文件数据
byte[] fileData = Files.readAllBytes(file.toPath());
String fileName = file.getName();
String fileType = getFileType(fileName);
// 根据文件类型创建不同的消息
MessageType msgType = determineMessageType(fileType);
Message message = new Message(msgType);
message.setSender(username);
message.setReceiver(receiver);
message.setFileName(fileName);
message.setFileData(fileData);
message.setFileType(fileType);
message.setFileSize(fileData.length);
System.out.println("正在发送文件: " + fileName + " (" + formatFileSize(fileData.length) + ")");
sendMessage(message);
System.out.println("文件发送完成");
} catch (IOException e) {
System.err.println("发送文件失败: " + e.getMessage());
}
}
/**
*
*/
public void sendVoice(String receiver, byte[] voiceData, int duration) {
Message message = new Message(MessageType.VOICE);
message.setSender(username);
message.setReceiver(receiver);
message.setFileData(voiceData);
message.setDuration(duration);
message.setFileName("voice_" + System.currentTimeMillis() + ".wav");
message.setFileType("audio/wav");
sendMessage(message);
}
/**
*
*/
private MessageType determineMessageType(String fileType) {
if (fileType.startsWith("image/")) {
return MessageType.IMAGE;
} else if (fileType.startsWith("video/")) {
return MessageType.VIDEO;
} else if (fileType.startsWith("audio/")) {
return MessageType.AUDIO;
} else {
return MessageType.FILE;
}
}
/**
* MIME
*/
private String getFileType(String fileName) {
String extension = "";
int i = fileName.lastIndexOf('.');
if (i > 0) {
extension = fileName.substring(i + 1).toLowerCase();
}
// 图片类型
switch (extension) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "bmp":
return "image/bmp";
case "webp":
return "image/webp";
// 视频类型
case "mp4":
return "video/mp4";
case "avi":
return "video/x-msvideo";
case "mov":
return "video/quicktime";
case "wmv":
return "video/x-ms-wmv";
case "flv":
return "video/x-flv";
case "webm":
return "video/webm";
// 音频类型
case "mp3":
return "audio/mpeg";
case "wav":
return "audio/wav";
case "ogg":
return "audio/ogg";
case "m4a":
return "audio/mp4";
case "flac":
return "audio/flac";
default:
return "application/octet-stream";
}
}
/**
*
*/
private String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.2f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.2f MB", size / (1024.0 * 1024));
} else {
return String.format("%.2f GB", size / (1024.0 * 1024 * 1024));
}
}
private synchronized void sendMessage(Message message) {
try {
output.writeObject(message);
@ -134,10 +278,72 @@ public class ChatClient {
System.out.println("\n[" + message.getSender() + "]: " + message.getContent());
break;
case FILE:
case IMAGE:
case VIDEO:
case AUDIO:
handleFileMessage(message);
break;
case VOICE:
handleVoiceMessage(message);
break;
default:
break;
}
}
private void handleFileMessage(Message message) {
try {
String fileName = message.getFileName();
byte[] fileData = message.getFileData();
// 保存文件
String savePath = DOWNLOAD_DIR + File.separator + username + "_" + fileName;
Files.write(Paths.get(savePath), fileData);
String typeDesc = getTypeDescription(message.getType());
System.out.println("\n[" + message.getSender() + "] 发送了" + typeDesc + ": " + fileName);
System.out.println("文件大小: " + formatFileSize(fileData.length));
System.out.println("已保存到: " + savePath + "\n");
} catch (IOException e) {
System.err.println("保存文件失败: " + e.getMessage());
}
}
private void handleVoiceMessage(Message message) {
try {
String fileName = message.getFileName();
byte[] voiceData = message.getFileData();
int duration = message.getDuration();
// 保存语音文件
String savePath = DOWNLOAD_DIR + File.separator + username + "_" + fileName;
Files.write(Paths.get(savePath), voiceData);
System.out.println("\n[" + message.getSender() + "] 发送了语音消息 (" + duration + "秒)");
System.out.println("已保存到: " + savePath + "\n");
} catch (IOException e) {
System.err.println("保存语音失败: " + e.getMessage());
}
}
private String getTypeDescription(MessageType type) {
switch (type) {
case IMAGE:
return "图片";
case VIDEO:
return "视频";
case AUDIO:
return "音频";
case FILE:
default:
return "文件";
}
}
}
public static void main(String[] args) {
@ -161,8 +367,9 @@ public class ChatClient {
if (client.connect(username)) {
System.out.println("\n命令说明:");
System.out.println(" @用户名 消息内容 - 发送私聊消息");
System.out.println(" quit - 退出程序\n");
System.out.println(" @用户名 消息内容 - 发送私聊消息");
System.out.println(" /file @用户名 文件路径 - 发送文件");
System.out.println(" quit - 退出程序\n");
// 主循环处理用户输入
while (client.running) {
@ -172,7 +379,17 @@ public class ChatClient {
break;
}
if (input.startsWith("@")) {
if (input.startsWith("/file ")) {
// 文件发送命令
String[] parts = input.substring(6).split(" ", 2);
if (parts.length == 2 && parts[0].startsWith("@")) {
String receiver = parts[0].substring(1);
String filePath = parts[1];
client.sendFile(receiver, filePath);
} else {
System.out.println("格式错误,请使用: /file @用户名 文件路径");
}
} else if (input.startsWith("@")) {
int spaceIndex = input.indexOf(' ');
if (spaceIndex > 1) {
String receiver = input.substring(1, spaceIndex);
@ -182,7 +399,7 @@ public class ChatClient {
System.out.println("格式错误,请使用: @用户名 消息内容");
}
} else if (!input.isEmpty()) {
System.out.println("格式错误,请使用: @用户名 消息内容");
System.out.println("格式错误,请使用: @用户名 消息内容 或 /file @用户名 文件路径");
}
}

@ -16,6 +16,9 @@ public class Message implements Serializable {
private Date timestamp;
private byte[] fileData;
private String fileName;
private String fileType; // 文件MIME类型
private long fileSize; // 文件大小
private int duration; // 音视频时长(秒)
public Message(MessageType type) {
this.type = type;
@ -77,4 +80,28 @@ public class Message implements Serializable {
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFileType() {
return fileType;
}
public void setFileType(String fileType) {
this.fileType = fileType;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
}

@ -7,7 +7,11 @@ public enum MessageType {
LOGIN, // 登录
LOGOUT, // 登出
TEXT, // 文本消息
FILE, // 文件传输
FILE, // 普通文件传输
IMAGE, // 图片文件传输
VIDEO, // 视频文件传输
AUDIO, // 音频文件传输
VOICE, // 语音消息
USER_LIST, // 用户列表
PRIVATE_MSG, // 私聊消息
HEARTBEAT, // 心跳包

@ -67,6 +67,12 @@ public class ClientHandler implements Runnable {
break;
case PRIVATE_MSG:
case FILE:
case IMAGE:
case VIDEO:
case AUDIO:
case VOICE:
// 转发所有类型的消息(包括文件和多媒体)
server.sendPrivateMessage(message);
break;

@ -133,6 +133,17 @@ public class WebSocketClient implements Runnable {
String content = (String) message.get("content");
System.out.println("转发消息: " + sender + " -> " + receiver);
server.sendPrivateMessage(sender, receiver, content);
} else if ("FILE".equals(type) || "IMAGE".equals(type) ||
"VIDEO".equals(type) || "AUDIO".equals(type) || "VOICE".equals(type)) {
// 转发文件和多媒体消息
String sender = (String) message.get("sender");
String receiver = (String) message.get("receiver");
String fileName = (String) message.get("fileName");
System.out.println("转发" + type + ": " + fileName + " from " + sender + " to " + receiver);
// 添加时间戳
message.put("timestamp", System.currentTimeMillis());
server.forwardMediaMessage(message);
}
} catch (Exception e) {
System.err.println("处理消息失败: " + e.getMessage());

@ -272,6 +272,26 @@ public class WebSocketServer {
}
}
/**
*
*/
public void forwardMediaMessage(Map<String, Object> messageData) {
String receiver = (String) messageData.get("receiver");
String sender = (String) messageData.get("sender");
WebSocketClient receiverClient = clients.get(receiver);
WebSocketClient senderClient = clients.get(sender);
if (receiverClient != null) {
receiverClient.sendMessage(gson.toJson(messageData));
} else if (senderClient != null) {
Map<String, Object> error = new HashMap<>();
error.put("type", "ERROR");
error.put("content", "用户 " + receiver + " 不在线");
senderClient.sendMessage(gson.toJson(error));
}
}
private void broadcastUserList() {
Map<String, Object> message = new HashMap<>();
message.put("type", "USER_LIST");

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

@ -46,6 +46,15 @@
</div>
<div class="input-area">
<input
type="file"
id="fileInput"
style="display: none;"
accept="*/*"
onchange="handleFileSelect(event)">
<button class="attach-btn" onclick="document.getElementById('fileInput').click()" id="attachBtn" disabled title="发送文件">
📎
</button>
<input
type="text"
id="messageInput"
@ -53,6 +62,9 @@
disabled
autocomplete="off"
maxlength="500">
<button class="voice-btn" onclick="toggleVoiceRecording()" id="voiceBtn" disabled title="语音消息">
🎤
</button>
<button onclick="sendMessage()" id="sendBtn" disabled>
<span>发送</span>
</button>

@ -550,3 +550,179 @@ body {
font-size: 13px;
}
}
/* 文件和多媒体样式 */
.attach-btn, .voice-btn {
padding: 14px 18px;
background: #f5f5f5;
color: #666;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
transition: all 0.3s;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.attach-btn:hover:not(:disabled), .voice-btn:hover:not(:disabled) {
background: #e0e0e0;
transform: scale(1.1);
}
.attach-btn:disabled, .voice-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 文件消息样式 */
.message-file {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
margin-bottom: 6px;
}
.message.received .message-file {
background: #f5f5f5;
}
.file-icon {
font-size: 32px;
flex-shrink: 0;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.file-name {
font-weight: 600;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 12px;
opacity: 0.7;
}
.download-btn {
padding: 8px 16px;
background: rgba(255, 255, 255, 0.2);
color: inherit;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
flex-shrink: 0;
}
.message.received .download-btn {
background: white;
color: #667eea;
border-color: #667eea;
}
.download-btn:hover {
transform: scale(1.05);
opacity: 0.9;
}
/* 图片消息样式 */
.message-image {
max-width: 300px;
max-height: 300px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
display: block;
margin-bottom: 8px;
}
.message-image:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* 视频消息样式 */
.message-video {
max-width: 400px;
max-height: 300px;
border-radius: 12px;
display: block;
margin-bottom: 8px;
}
/* 音频消息样式 */
.message-audio {
width: 300px;
margin-bottom: 8px;
}
/* 语音消息样式 */
.message-voice {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
margin-bottom: 6px;
}
.message.received .message-voice {
background: #f5f5f5;
}
.voice-icon {
font-size: 24px;
flex-shrink: 0;
}
.voice-audio {
flex: 1;
height: 32px;
}
.voice-duration {
font-size: 12px;
opacity: 0.7;
flex-shrink: 0;
}
/* 响应式调整 */
@media (max-width: 768px) {
.message-image {
max-width: 200px;
max-height: 200px;
}
.message-video {
max-width: 280px;
max-height: 200px;
}
.message-audio {
width: 220px;
}
.attach-btn, .voice-btn {
width: 42px;
height: 42px;
font-size: 16px;
}
}

Loading…
Cancel
Save