添加聊天功能
This commit is contained in:
@@ -191,3 +191,104 @@ def api_contacts_update():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"更新通讯录失败: {e}")
|
logger.error(f"更新通讯录失败: {e}")
|
||||||
return jsonify({"success": False, "message": f"更新通讯录失败: {str(e)}"}), 500
|
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
|
||||||
|
|||||||
@@ -111,6 +111,12 @@
|
|||||||
{% raw %}@click="viewUserDetails(scope.row)" {% endraw %}>
|
{% raw %}@click="viewUserDetails(scope.row)" {% endraw %}>
|
||||||
查看详情
|
查看详情
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="success"
|
||||||
|
{% raw %}@click="openChatDialog(scope.row)" {% endraw %}>
|
||||||
|
聊天
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -347,6 +353,89 @@
|
|||||||
<!-- 可以添加更多公共好友相关信息 -->
|
<!-- 可以添加更多公共好友相关信息 -->
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 聊天对话框 -->
|
||||||
|
<el-dialog title="聊天" {% raw %}:visible.sync="chatDialogVisible" {% endraw %} width="60%" :close-on-click-modal="false">
|
||||||
|
<div class="chat-container">
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div class="message-list" ref="messageList">
|
||||||
|
<div v-for="(msg, index) in chatMessages" :key="index" class="message-item" :class="{'message-self': msg.isSelf}">
|
||||||
|
<div class="message-content">
|
||||||
|
<div v-if="msg.type === 'text'">{{ msg.content }}</div>
|
||||||
|
<div v-else-if="msg.type === 'image'">
|
||||||
|
<img :src="msg.content" style="max-width: 200px; max-height: 200px;">
|
||||||
|
</div>
|
||||||
|
<div v-else-if="msg.type === 'voice'">
|
||||||
|
<audio controls :src="msg.content"></audio>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="msg.type === 'video'">
|
||||||
|
<video controls :src="msg.content" style="max-width: 200px;"></video>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="msg.type === 'link'">
|
||||||
|
<a :href="msg.content.url" target="_blank">{{ msg.content.title }}</a>
|
||||||
|
<p>{{ msg.content.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-time">{{ msg.time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输入区域 -->
|
||||||
|
<div class="input-area">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入消息..."
|
||||||
|
{% raw %}v-model="messageInput" {% endraw %}
|
||||||
|
@keyup.enter.native="sendTextMessage">
|
||||||
|
</el-input>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-upload
|
||||||
|
class="upload-demo"
|
||||||
|
action="#"
|
||||||
|
:http-request="uploadImage"
|
||||||
|
:show-file-list="false">
|
||||||
|
<el-button size="small" type="primary">图片</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-upload
|
||||||
|
class="upload-demo"
|
||||||
|
action="#"
|
||||||
|
:http-request="uploadVoice"
|
||||||
|
:show-file-list="false">
|
||||||
|
<el-button size="small" type="primary">语音</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-upload
|
||||||
|
class="upload-demo"
|
||||||
|
action="#"
|
||||||
|
:http-request="uploadVideo"
|
||||||
|
:show-file-list="false">
|
||||||
|
<el-button size="small" type="primary">视频</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-button size="small" type="primary" @click="showLinkDialog">链接</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="sendTextMessage">发送</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 链接消息对话框 -->
|
||||||
|
<el-dialog title="发送链接" {% raw %}:visible.sync="linkDialogVisible" {% endraw %} width="30%">
|
||||||
|
<el-form :model="linkForm" label-width="80px">
|
||||||
|
<el-form-item label="链接">
|
||||||
|
<el-input v-model="linkForm.url" placeholder="请输入链接"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="linkForm.title" placeholder="请输入标题"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input type="textarea" v-model="linkForm.description" placeholder="请输入描述"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="linkDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="sendLinkMessage">发送</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -386,7 +475,17 @@
|
|||||||
groupMembersCurrentPage: 1,
|
groupMembersCurrentPage: 1,
|
||||||
groupMembersPageSize: 10,
|
groupMembersPageSize: 10,
|
||||||
groupMemberSearchQuery: '',
|
groupMemberSearchQuery: '',
|
||||||
groupMembersLoading: false
|
groupMembersLoading: false,
|
||||||
|
chatDialogVisible: false,
|
||||||
|
currentChatUser: null,
|
||||||
|
messageInput: '',
|
||||||
|
chatMessages: [],
|
||||||
|
linkDialogVisible: false,
|
||||||
|
linkForm: {
|
||||||
|
url: '',
|
||||||
|
title: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -662,6 +761,167 @@
|
|||||||
},
|
},
|
||||||
handleGroupMembersCurrentChange(page) {
|
handleGroupMembersCurrentChange(page) {
|
||||||
this.groupMembersCurrentPage = 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;
|
font-size: 18px;
|
||||||
color: #303133;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user