diff --git a/admin/dashboard/blueprints/contacts.py b/admin/dashboard/blueprints/contacts.py index 0480da9..c20e4a5 100644 --- a/admin/dashboard/blueprints/contacts.py +++ b/admin/dashboard/blueprints/contacts.py @@ -191,3 +191,104 @@ def api_contacts_update(): except Exception as e: logger.error(f"更新通讯录失败: {e}") return jsonify({"success": False, "message": f"更新通讯录失败: {str(e)}"}), 500 + + +@contacts_bp.route('/api/send_message', methods=['POST']) +@login_required +async def api_send_message(): + """发送消息API + + 支持的消息类型: + - text: 文本消息 + - image: 图片消息 + - voice: 语音消息 + - video: 视频消息 + - link: 链接消息 + """ + try: + data = request.form if request.files else request.json + wxid = data.get('wxid') + msg_type = data.get('type') + content = data.get('content') + + if not wxid or not msg_type: + return jsonify({'success': False, 'message': '缺少必要参数'}) + + # 获取机器人实例 + server = current_app.dashboard_server + if not server or not server.robot: + return jsonify({'success': False, 'message': '机器人未初始化'}) + + # 根据消息类型调用不同的发送方法 + if msg_type == 'text': + client_msg_id, create_time, new_msg_id = await server.robot.send_text_message(wxid, content) + return jsonify({ + 'success': True, + 'data': { + 'client_msg_id': client_msg_id, + 'create_time': create_time, + 'new_msg_id': new_msg_id + } + }) + + elif msg_type == 'image': + if 'file' not in request.files: + return jsonify({'success': False, 'message': '未上传文件'}) + file = request.files['file'] + client_msg_id, create_time, new_msg_id = await server.robot.send_image_message(wxid, file.read()) + return jsonify({ + 'success': True, + 'data': { + 'client_msg_id': client_msg_id, + 'create_time': create_time, + 'new_msg_id': new_msg_id + } + }) + + elif msg_type == 'voice': + if 'file' not in request.files: + return jsonify({'success': False, 'message': '未上传文件'}) + file = request.files['file'] + client_msg_id, create_time, new_msg_id = await server.robot.send_voice_message(wxid, file.read()) + return jsonify({ + 'success': True, + 'data': { + 'client_msg_id': client_msg_id, + 'create_time': create_time, + 'new_msg_id': new_msg_id + } + }) + + elif msg_type == 'video': + if 'file' not in request.files: + return jsonify({'success': False, 'message': '未上传文件'}) + file = request.files['file'] + client_msg_id, new_msg_id = await server.robot.send_video_message(wxid, file.read()) + return jsonify({ + 'success': True, + 'data': { + 'client_msg_id': client_msg_id, + 'new_msg_id': new_msg_id + } + }) + + elif msg_type == 'link': + url = content.get('url') + title = content.get('title', '') + description = content.get('description', '') + client_msg_id, create_time, new_msg_id = await server.robot.send_link_message(wxid, url, title, description) + return jsonify({ + 'success': True, + 'data': { + 'client_msg_id': client_msg_id, + 'create_time': create_time, + 'new_msg_id': new_msg_id + } + }) + + else: + return jsonify({'success': False, 'message': '不支持的消息类型'}) + + except Exception as e: + logger.error(f"发送消息失败: {e}") + return jsonify({'success': False, 'message': str(e)}), 500 diff --git a/admin/dashboard/templates/contacts_management.html b/admin/dashboard/templates/contacts_management.html index 5e6253c..e6f9eb7 100644 --- a/admin/dashboard/templates/contacts_management.html +++ b/admin/dashboard/templates/contacts_management.html @@ -111,6 +111,12 @@ {% raw %}@click="viewUserDetails(scope.row)" {% endraw %}> 查看详情 + + 聊天 + @@ -347,6 +353,89 @@ + + + + + + + + + {{ msg.content }} + + + + + + + + + + + {{ msg.content.title }} + {{ msg.content.description }} + + + {{ msg.time }} + + + + + + + + + + 图片 + + + 语音 + + + 视频 + + 链接 + 发送 + + + + + + + + + + + + + + + + + + + + 取消 + 发送 + + {% endblock %} @@ -386,7 +475,17 @@ groupMembersCurrentPage: 1, groupMembersPageSize: 10, groupMemberSearchQuery: '', - groupMembersLoading: false + groupMembersLoading: false, + chatDialogVisible: false, + currentChatUser: null, + messageInput: '', + chatMessages: [], + linkDialogVisible: false, + linkForm: { + url: '', + title: '', + description: '' + } }; }, computed: { @@ -662,6 +761,167 @@ }, handleGroupMembersCurrentChange(page) { this.groupMembersCurrentPage = page; + }, + // 打开聊天对话框 + openChatDialog(user) { + this.currentChatUser = user; + this.chatDialogVisible = true; + this.chatMessages = []; + }, + + // 发送文本消息 + async sendTextMessage() { + if (!this.messageInput.trim()) return; + + try { + const response = await axios.post('/contacts/api/send_message', { + wxid: this.currentChatUser.wxid, + type: 'text', + content: this.messageInput + }); + + if (response.data.success) { + this.chatMessages.push({ + type: 'text', + content: this.messageInput, + isSelf: true, + time: new Date().toLocaleTimeString() + }); + this.messageInput = ''; + this.$nextTick(() => { + this.scrollToBottom(); + }); + } + } catch (error) { + this.$message.error('发送消息失败'); + } + }, + + // 上传图片 + async uploadImage(options) { + const file = options.file; + const formData = new FormData(); + formData.append('file', file); + formData.append('wxid', this.currentChatUser.wxid); + formData.append('type', 'image'); + + try { + const response = await axios.post('/contacts/api/send_message', formData); + if (response.data.success) { + this.chatMessages.push({ + type: 'image', + content: response.data.url, + isSelf: true, + time: new Date().toLocaleTimeString() + }); + this.$nextTick(() => { + this.scrollToBottom(); + }); + } + } catch (error) { + this.$message.error('发送图片失败'); + } + }, + + // 上传语音 + async uploadVoice(options) { + const file = options.file; + const formData = new FormData(); + formData.append('file', file); + formData.append('wxid', this.currentChatUser.wxid); + formData.append('type', 'voice'); + + try { + const response = await axios.post('/contacts/api/send_message', formData); + if (response.data.success) { + this.chatMessages.push({ + type: 'voice', + content: response.data.url, + isSelf: true, + time: new Date().toLocaleTimeString() + }); + this.$nextTick(() => { + this.scrollToBottom(); + }); + } + } catch (error) { + this.$message.error('发送语音失败'); + } + }, + + // 上传视频 + async uploadVideo(options) { + const file = options.file; + const formData = new FormData(); + formData.append('file', file); + formData.append('wxid', this.currentChatUser.wxid); + formData.append('type', 'video'); + + try { + const response = await axios.post('/contacts/api/send_message', formData); + if (response.data.success) { + this.chatMessages.push({ + type: 'video', + content: response.data.url, + isSelf: true, + time: new Date().toLocaleTimeString() + }); + this.$nextTick(() => { + this.scrollToBottom(); + }); + } + } catch (error) { + this.$message.error('发送视频失败'); + } + }, + + // 显示链接对话框 + showLinkDialog() { + this.linkForm = { + url: '', + title: '', + description: '' + }; + this.linkDialogVisible = true; + }, + + // 发送链接消息 + async sendLinkMessage() { + if (!this.linkForm.url) { + this.$message.warning('请输入链接'); + return; + } + + try { + const response = await axios.post('/contacts/api/send_message', { + wxid: this.currentChatUser.wxid, + type: 'link', + content: this.linkForm + }); + + if (response.data.success) { + this.chatMessages.push({ + type: 'link', + content: this.linkForm, + isSelf: true, + time: new Date().toLocaleTimeString() + }); + this.linkDialogVisible = false; + this.$nextTick(() => { + this.scrollToBottom(); + }); + } + } catch (error) { + this.$message.error('发送链接失败'); + } + }, + + // 滚动到底部 + scrollToBottom() { + const messageList = this.$refs.messageList; + if (messageList) { + messageList.scrollTop = messageList.scrollHeight; + } } }, }); @@ -704,5 +964,58 @@ font-size: 18px; color: #303133; } + + .chat-container { + display: flex; + flex-direction: column; + height: 500px; + } + + .message-list { + flex: 1; + overflow-y: auto; + padding: 20px; + background: #f5f5f5; + } + + .message-item { + margin-bottom: 15px; + display: flex; + flex-direction: column; + } + + .message-self { + align-items: flex-end; + } + + .message-content { + max-width: 70%; + padding: 10px; + border-radius: 5px; + background: #fff; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); + } + + .message-self .message-content { + background: #95ec69; + } + + .message-time { + font-size: 12px; + color: #999; + margin-top: 5px; + } + + .input-area { + padding: 20px; + background: #fff; + border-top: 1px solid #eee; + } + + .toolbar { + margin-top: 10px; + display: flex; + gap: 10px; + } {% endblock %} \ No newline at end of file
{{ msg.content.description }}