diff --git a/goldminer/backend/app.py b/goldminer/backend/app.py index 49dd79e9..edec19da 100644 --- a/goldminer/backend/app.py +++ b/goldminer/backend/app.py @@ -20,6 +20,28 @@ app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产 app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' +# 确保CORS配置正确处理凭证 +CORS(app, + supports_credentials=True, + resources={r"/api/*": {"origins": "*"}}, + methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allow_headers=['Content-Type', 'Authorization', 'X-Requested-With']) + +# 添加OPTIONS请求的全局处理,确保CORS预检请求能够正确响应 +@app.after_request +def after_request(response): + # 移除可能已存在的CORS头部 + if 'Access-Control-Allow-Origin' in response.headers: + del response.headers['Access-Control-Allow-Origin'] + + origin = request.headers.get('Origin') + if origin: + response.headers.set('Access-Control-Allow-Origin', origin) + response.headers.set('Access-Control-Allow-Headers', 'Content-Type,Authorization') + response.headers.set('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS') + response.headers.set('Access-Control-Allow-Credentials', 'true') + return response + # 云端MySQL数据库配置 DB_USER = os.environ.get('DB_USER', 'goldminer') DB_PASSWORD = os.environ.get('DB_PASSWORD', 'nBAWq9DDwJ14Fugq') @@ -34,15 +56,15 @@ app.config['SQLALCHEMY_ECHO'] = True # 开启SQL查询日志,方便调试 logger.info(f"连接到云端数据库: {DB_HOST}:{DB_PORT}/{DB_NAME}") -# 确保CORS配置正确处理凭证 -CORS(app, - supports_credentials=True, - origins=['*'], - methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allow_headers=['Content-Type', 'Authorization', 'X-Requested-With']) # 允许所有源的凭证请求 - db = SQLAlchemy(app) -socketio = SocketIO(app, cors_allowed_origins="*", engineio_logger=True, async_mode='threading') +socketio = SocketIO(app, + cors_allowed_origins="*", + engineio_logger=True, + async_mode='threading', + logger=True, + ping_timeout=60, + ping_interval=25, + always_connect=True) # 跟踪所有活跃游戏会话 active_games = {} # 用户ID -> 游戏状态 @@ -97,6 +119,8 @@ class ChatMessage(db.Model): room_id = db.Column(db.Integer, db.ForeignKey('chat_room.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) message = db.Column(db.Text, nullable=False) + message_type = db.Column(db.String(20), default='text') # 'text' 或 'audio' + audio_duration = db.Column(db.Float, nullable=True) # 语音消息的持续时间(秒) created_at = db.Column(db.DateTime, default=datetime.utcnow) # 建立关系 @@ -114,6 +138,26 @@ def init_db(): # 检查是否需要创建默认管理员账户 create_default_admin() + + # 检查是否需要添加新的列到ChatMessage表 + try: + # 检查列是否已存在并添加 + inspector = db.inspect(db.engine) + columns = [column['name'] for column in inspector.get_columns('chat_message')] + + # 添加message_type列(如果不存在) + if 'message_type' not in columns: + db.engine.execute("ALTER TABLE chat_message ADD COLUMN message_type VARCHAR(20) DEFAULT 'text'") + logger.info("已添加message_type列到ChatMessage表") + + # 添加audio_duration列(如果不存在) + if 'audio_duration' not in columns: + db.engine.execute("ALTER TABLE chat_message ADD COLUMN audio_duration FLOAT NULL") + logger.info("已添加audio_duration列到ChatMessage表") + + logger.info("已检查并更新ChatMessage表结构。") + except Exception as e: + logger.warning(f"更新ChatMessage表结构时出现问题: {e}") except OperationalError as e: logger.error("无法连接到云端MySQL数据库。请检查您的数据库配置和网络连接。") logger.error(f"错误详情: {e}") @@ -438,13 +482,20 @@ def get_room_messages(room_id): message_list = [] for msg in messages: user = User.query.get(msg.user_id) - message_list.append({ + message_data = { 'id': msg.id, 'user_id': msg.user_id, 'username': user.username if user else 'Unknown', 'message': msg.message, + 'message_type': msg.message_type, 'created_at': msg.created_at.isoformat() - }) + } + + # 如果是语音消息,添加持续时间 + if msg.message_type == 'audio' and msg.audio_duration is not None: + message_data['audio_duration'] = msg.audio_duration + + message_list.append(message_data) return jsonify({'messages': message_list}) @@ -570,7 +621,8 @@ def handle_send_message(data): message = ChatMessage( room_id=room_id, user_id=user_id, - message=message_text + message=message_text, + message_type='text' ) db.session.add(message) @@ -582,6 +634,50 @@ def handle_send_message(data): 'user_id': user_id, 'username': user.username, 'message': message_text, + 'message_type': 'text', + 'created_at': message.created_at.isoformat() + }, room=str(room_id)) + +@socketio.on('send_audio_message') +def handle_send_audio_message(data): + user_id = session.get('user_id') + if not user_id: + return + + room_id = data.get('room_id') + audio_data = data.get('audio_data') # Base64编码的音频数据 + audio_duration = data.get('audio_duration', 0) # 音频持续时间(秒) + + if not room_id or not audio_data: + return + + # 获取用户和房间信息 + user = User.query.get(user_id) + room = ChatRoom.query.get(room_id) + + if not user or not room: + return + + # 保存消息到数据库 + message = ChatMessage( + room_id=room_id, + user_id=user_id, + message=audio_data, # 存储Base64编码的音频数据 + message_type='audio', + audio_duration=audio_duration + ) + + db.session.add(message) + db.session.commit() + + # 广播消息给房间内的所有用户 + emit('new_message', { + 'id': message.id, + 'user_id': user_id, + 'username': user.username, + 'message': audio_data, + 'message_type': 'audio', + 'audio_duration': audio_duration, 'created_at': message.created_at.isoformat() }, room=str(room_id)) @@ -922,6 +1018,11 @@ def test_admin_status(): } }) +# 处理所有OPTIONS请求 +@app.route('/api/', methods=['OPTIONS']) +def handle_options(path): + return '', 200 + if __name__ == '__main__': # 在启动应用前初始化数据库 init_db() diff --git a/goldminer/backend/udp_client_simulator.py b/goldminer/backend/udp_client_simulator.py new file mode 100644 index 00000000..59e62125 --- /dev/null +++ b/goldminer/backend/udp_client_simulator.py @@ -0,0 +1,51 @@ +import socket +import time +import random + +# --- 配置 --- +# 您的后端服务器的IP地址。 +# 如果您在另一台电脑上运行此脚本,请将其更改为运行后端的电脑的IP地址。 +# 如果在同一台电脑上,使用 '127.0.0.1' 即可。 +SERVER_IP = '127.0.0.1' +SERVER_PORT = 5001 # 必须与 app.py 中UDP服务器的端口匹配 +CLIENT_ID = f'player_{random.randint(1000, 9999)}' +SEND_INTERVAL = 0.5 # 每隔多少秒发送一次数据(模拟游戏帧率) +# --- + +def run_udp_client(): + """ + 模拟一个游戏客户端,通过UDP协议向服务器发送高频的游戏状态更新。 + """ + # 创建一个UDP套接字 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client_socket: + print(f"UDP客户端 '{CLIENT_ID}' 已启动,将向 {SERVER_IP}:{SERVER_PORT} 发送数据...") + print("按 Ctrl+C 停止。") + + hook_angle = 0 + direction = 1 + + try: + while True: + # 模拟钩子来回摆动 + if hook_angle > 60: + direction = -1 + elif hook_angle < -60: + direction = 1 + + hook_angle += direction * 5 + + # 构造消息 + message = f"client:{CLIENT_ID};hook_angle:{hook_angle};timestamp:{time.time()}" + + # 发送数据 + client_socket.sendto(message.encode('utf-8'), (SERVER_IP, SERVER_PORT)) + print(f"已发送 -> {message}") + + # 等待一段时间再发送下一次更新 + time.sleep(SEND_INTERVAL) + + except KeyboardInterrupt: + print(f"\n客户端 '{CLIENT_ID}' 已停止。") + +if __name__ == "__main__": + run_udp_client() \ No newline at end of file diff --git a/goldminer/frontend/node_modules/.cache/eslint/123ee647.json b/goldminer/frontend/node_modules/.cache/eslint/123ee647.json index 832189ab..c96a0e19 100644 --- a/goldminer/frontend/node_modules/.cache/eslint/123ee647.json +++ b/goldminer/frontend/node_modules/.cache/eslint/123ee647.json @@ -1 +1 @@ -[{"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js":"1","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue":"2","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js":"3","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue":"4","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue":"5","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue":"6","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue":"7","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue":"8","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue":"9","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue":"10","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue":"11","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue":"12","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue":"13"},{"size":1590,"mtime":1750303858492,"results":"14","hashOfConfig":"15"},{"size":1004,"mtime":1750128639386,"results":"16","hashOfConfig":"15"},{"size":2623,"mtime":1750303858493,"results":"17","hashOfConfig":"15"},{"size":5735,"mtime":1750209720346,"results":"18","hashOfConfig":"15"},{"size":4492,"mtime":1750147385471,"results":"19","hashOfConfig":"15"},{"size":51882,"mtime":1750389761182,"results":"20","hashOfConfig":"15"},{"size":5329,"mtime":1750336307891,"results":"21","hashOfConfig":"15"},{"size":11652,"mtime":1750381631833,"results":"22","hashOfConfig":"15"},{"size":16278,"mtime":1750304819474,"results":"23","hashOfConfig":"15"},{"size":13225,"mtime":1750128639387,"results":"24","hashOfConfig":"15"},{"size":4886,"mtime":1750381631832,"results":"25","hashOfConfig":"15"},{"size":19436,"mtime":1750391010410,"results":"26","hashOfConfig":"15"},{"size":4836,"mtime":1750303858491,"results":"27","hashOfConfig":"15"},{"filePath":"28","messages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"i331ru",{"filePath":"30","messages":"31","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"32","messages":"33","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"38","messages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"42","messages":"43","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"44","messages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"50","messages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"52","messages":"53","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue",[]] \ No newline at end of file +[{"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js":"1","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue":"2","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js":"3","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue":"4","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue":"5","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue":"6","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue":"7","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue":"8","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue":"9","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue":"10","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue":"11","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue":"12","D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue":"13"},{"size":1590,"mtime":1750768017958,"results":"14","hashOfConfig":"15"},{"size":1004,"mtime":1750128639386,"results":"16","hashOfConfig":"15"},{"size":2623,"mtime":1750303858493,"results":"17","hashOfConfig":"15"},{"size":5735,"mtime":1750209720346,"results":"18","hashOfConfig":"15"},{"size":4492,"mtime":1750147385471,"results":"19","hashOfConfig":"15"},{"size":53697,"mtime":1750767242119,"results":"20","hashOfConfig":"15"},{"size":5329,"mtime":1750336307891,"results":"21","hashOfConfig":"15"},{"size":11652,"mtime":1750381631833,"results":"22","hashOfConfig":"15"},{"size":24471,"mtime":1750768438839,"results":"23","hashOfConfig":"15"},{"size":13225,"mtime":1750128639387,"results":"24","hashOfConfig":"15"},{"size":4886,"mtime":1750381631832,"results":"25","hashOfConfig":"15"},{"size":19436,"mtime":1750391010410,"results":"26","hashOfConfig":"15"},{"size":4836,"mtime":1750303858491,"results":"27","hashOfConfig":"15"},{"filePath":"28","messages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"i331ru",{"filePath":"30","messages":"31","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"33","messages":"34","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"35"},{"filePath":"36","messages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"38","messages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"40","messages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"42","messages":"43","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"44","messages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"46","messages":"47","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"48","messages":"49","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"50","messages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"52","messages":"53","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},{"filePath":"54","messages":"55","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"32"},"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\main.js",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\App.vue",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\router.js",[],[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Home.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Register.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Game.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Login.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\UserProfile.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\ChatRoom.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Leaderboard.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Header.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\Admin.vue",[],"D:\\学习\\网络应用程序开发\\pdf\\goldminer\\frontend\\src\\components\\AdminSetup.vue",[]] \ No newline at end of file diff --git a/goldminer/frontend/src/components/ChatRoom.vue b/goldminer/frontend/src/components/ChatRoom.vue index b35ca37a..4612d1b5 100644 --- a/goldminer/frontend/src/components/ChatRoom.vue +++ b/goldminer/frontend/src/components/ChatRoom.vue @@ -67,9 +67,27 @@ {{ formatTime(msg.created_at) }} -
+ + +
{{ msg.message }}
+ + +
+
+ +
+
{{ formatAudioDuration(msg.audio_duration) }}
+
+
+
@@ -88,7 +106,20 @@ placeholder="输入消息..." @keyup.enter="sendMessage" /> - + + + +
@@ -113,6 +144,9 @@ + + + @@ -134,6 +168,14 @@ export default { const newRoomName = ref('') const showCreateRoomModal = ref(false) const messagesContainer = ref(null) + const audioPlayer = ref(null) + + // 语音录制相关 + const isRecording = ref(false) + const recordingDuration = ref(0) + const mediaRecorder = ref(null) + const audioChunks = ref([]) + const recordingTimer = ref(null) let socket = null let currentUser = null @@ -159,14 +201,25 @@ export default { if (socket) { socket.disconnect() } + + // 清除录音计时器 + if (recordingTimer.value) { + clearInterval(recordingTimer.value) + } }) // 初始化Socket连接 const initSocketConnection = () => { // 创建Socket连接 - socket = io('//', { - withCredentials: true - }) + const currentHost = window.location.hostname; + const url = `http://${currentHost}:5000`; + + // 使用轮询模式避免WebSocket连接问题 + socket = io(url, { + withCredentials: true, + transports: ['polling'], + path: '/socket.io' + }); // 监听连接事件 socket.on('connect', () => { @@ -282,7 +335,7 @@ export default { notifications.value = [] } - // 发送消息 + // 发送文本消息 const sendMessage = () => { if (!currentRoom.value || !newMessage.value.trim()) return @@ -296,6 +349,142 @@ export default { newMessage.value = '' } + // 开始录音 + const startRecording = async () => { + try { + // 请求用户的麦克风权限 + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + + // 创建媒体录制器 + mediaRecorder.value = new MediaRecorder(stream) + audioChunks.value = [] + + // 监听数据可用事件 + mediaRecorder.value.addEventListener('dataavailable', event => { + if (event.data.size > 0) { + audioChunks.value.push(event.data) + } + }) + + // 监听录制完成事件 + mediaRecorder.value.addEventListener('stop', () => { + // 如果没有录制数据或录制被取消,直接返回 + if (audioChunks.value.length === 0 || !isRecording.value) { + isRecording.value = false + recordingDuration.value = 0 + stopMediaTracks(stream) + return + } + + // 将录制的音频块转换为Blob + const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm' }) + + // 将Blob转换为Base64 + const reader = new FileReader() + reader.readAsDataURL(audioBlob) + reader.onloadend = () => { + const base64Audio = reader.result + + // 发送语音消息 + socket.emit('send_audio_message', { + room_id: currentRoom.value.id, + audio_data: base64Audio, + audio_duration: recordingDuration.value + }) + + // 重置录音状态 + isRecording.value = false + recordingDuration.value = 0 + } + + // 停止所有媒体轨道 + stopMediaTracks(stream) + }) + + // 开始录音 + mediaRecorder.value.start() + isRecording.value = true + + // 启动计时器 + recordingTimer.value = setInterval(() => { + recordingDuration.value++ + + // 限制最大录音时间为60秒 + if (recordingDuration.value >= 60) { + stopRecording() + } + }, 1000) + } catch (err) { + console.error('无法访问麦克风:', err) + alert('无法访问麦克风,请检查浏览器权限设置。') + } + } + + // 停止录音 + const stopRecording = () => { + if (!mediaRecorder.value || mediaRecorder.value.state === 'inactive') { + return + } + + // 停止录音 + mediaRecorder.value.stop() + + // 清除计时器 + if (recordingTimer.value) { + clearInterval(recordingTimer.value) + recordingTimer.value = null + } + } + + // 取消录音 + const cancelRecording = () => { + if (isRecording.value) { + isRecording.value = false + + if (mediaRecorder.value && mediaRecorder.value.state === 'recording') { + mediaRecorder.value.stop() + } + + // 清除计时器 + if (recordingTimer.value) { + clearInterval(recordingTimer.value) + recordingTimer.value = null + } + + recordingDuration.value = 0 + audioChunks.value = [] + } + } + + // 停止所有媒体轨道 + const stopMediaTracks = (stream) => { + stream.getTracks().forEach(track => track.stop()) + } + + // 播放音频 + const playAudio = (message) => { + if (!message || message.message_type !== 'audio') return + + if (audioPlayer.value) { + // 停止当前正在播放的音频 + audioPlayer.value.pause() + audioPlayer.value.currentTime = 0 + + // 设置新的音频源 + audioPlayer.value.src = message.message + audioPlayer.value.play() + } + } + + // 格式化音频时长 + const formatAudioDuration = (seconds) => { + if (!seconds) return '0:00' + + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins}:${secs.toString().padStart(2, '0')}` + } + // 添加系统通知 const addNotification = (message) => { notifications.value.push({ @@ -343,11 +532,19 @@ export default { newRoomName, showCreateRoomModal, messagesContainer, + audioPlayer, + isRecording, + recordingDuration, fetchRooms, createRoom, joinRoom, leaveRoom, sendMessage, + startRecording, + stopRecording, + cancelRecording, + playAudio, + formatAudioDuration, isCurrentUser, formatTime } @@ -576,6 +773,53 @@ export default { color: white; } +/* 语音消息样式 */ +.audio-message { + min-width: 120px; +} + +.audio-player { + display: flex; + align-items: center; + gap: 10px; +} + +.play-button { + background: none; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + color: inherit; +} + +.audio-icon { + font-size: 20px; +} + +.audio-waveform { + flex: 1; + height: 24px; + background: rgba(255, 255, 255, 0.2); + border-radius: 12px; + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 10px; +} + +.current-user-content .audio-waveform { + background: rgba(0, 0, 0, 0.2); +} + +.duration { + font-size: 12px; +} + .no-messages { text-align: center; padding: 20px; @@ -613,20 +857,74 @@ export default { border-color: #8B4513; } -.message-input button { +.send-button { background-color: #8B4513; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; + margin-right: 10px; } -.message-input button:disabled { +.send-button:disabled { background-color: #ccc; cursor: not-allowed; } +/* 语音录制按钮样式 */ +.voice-button { + background-color: #f1f1f1; + border: none; + border-radius: 50%; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + position: relative; + transition: all 0.2s; +} + +.voice-button:hover { + background-color: #e0e0e0; +} + +.voice-button.recording { + background-color: #e74c3c; + color: white; + box-shadow: 0 0 0 5px rgba(231, 76, 60, 0.3); + animation: pulse 1.5s infinite; +} + +.mic-icon { + font-size: 20px; +} + +.recording-text { + position: absolute; + bottom: -25px; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + font-size: 12px; + color: #e74c3c; + font-weight: bold; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(231, 76, 60, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); + } +} + /* 模态框 */ .modal-overlay { position: fixed; diff --git a/goldminer/restart_backend.bat b/goldminer/restart_backend.bat new file mode 100644 index 00000000..78a633fb --- /dev/null +++ b/goldminer/restart_backend.bat @@ -0,0 +1,7 @@ +@echo off +echo 正在停止后端服务... +taskkill /f /im python.exe + +echo 正在启动后端服务... +cd backend +python run_server.py \ No newline at end of file