@@ -403,46 +403,9 @@ def api_send_message():
|
||||
'message': '消息发送中'
|
||||
})
|
||||
|
||||
elif msg_type == 'emoji':
|
||||
md5 = content.get('md5') if isinstance(content, dict) else None
|
||||
total_length = int((content or {}).get('total_length') or 0) if isinstance(content, dict) else 0
|
||||
if not md5 or total_length <= 0:
|
||||
return jsonify({'success': False, 'message': '缺少表情参数'})
|
||||
send_message_in_thread(server.client.send_emoji_message, wxid, md5, total_length)
|
||||
server.emoji_asset_db.mark_sent(md5)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '表情发送中'
|
||||
})
|
||||
|
||||
else:
|
||||
return jsonify({'success': False, 'message': '不支持的消息类型'})
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"发送消息失败: {e}")
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@contacts_bp.route('/api/emoji_assets', methods=['GET'])
|
||||
@login_required
|
||||
def api_emoji_assets():
|
||||
"""获取表情资产列表API"""
|
||||
try:
|
||||
server = current_app.dashboard_server
|
||||
limit = min(max(int(request.args.get("limit", 60) or 60), 1), 200)
|
||||
roomid = request.args.get("roomid", "").strip()
|
||||
assets = server.emoji_asset_db.list_assets(limit=limit, chatroom_id=roomid)
|
||||
for asset in assets:
|
||||
source_wxid = asset.get("source_wxid", "")
|
||||
asset["source_name"] = server.contact_manager.get_nickname(source_wxid) or source_wxid
|
||||
source_chatroom_id = asset.get("source_chatroom_id", "")
|
||||
asset["source_chatroom_name"] = server.contact_manager.get_nickname(source_chatroom_id) or source_chatroom_id
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"assets": assets
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"获取表情资产列表失败: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@@ -11,7 +11,6 @@ from flask import Flask, send_from_directory
|
||||
from loguru import logger
|
||||
|
||||
from db.contacts_db import ContactsDBOperator
|
||||
from db.emoji_asset_db import EmojiAssetDBOperator
|
||||
from db.member_context_db import MemberContextDBOperator
|
||||
from db.message_storage import MessageStorageDB
|
||||
from db.stats_db import StatsDBOperator
|
||||
@@ -44,7 +43,6 @@ class DashboardServer:
|
||||
self.db_manager = robot_instance.db_manager
|
||||
self.stats_db = StatsDBOperator(self.db_manager)
|
||||
self.message_storage = MessageStorageDB(self.db_manager)
|
||||
self.emoji_asset_db = EmojiAssetDBOperator(self.db_manager)
|
||||
self.contact_db: ContactsDBOperator = ContactsDBOperator(self.db_manager)
|
||||
self.member_context_db = MemberContextDBOperator(self.db_manager)
|
||||
self.task_db: TaskDBOperator = TaskDBOperator(self.db_manager)
|
||||
|
||||
@@ -190,9 +190,9 @@
|
||||
<el-button size="mini" type="success" @click="refreshCurrentGroupContexts">刷新本群摘要</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="filteredGroupMembers" style="width: 100%" v-loading="groupMembersLoading" class="group-members-table">
|
||||
<el-table :data="filteredGroupMembers" style="width: 100%" v-loading="groupMembersLoading">
|
||||
<el-table-column type="index" width="54"></el-table-column>
|
||||
<el-table-column label="成员" min-width="280" show-overflow-tooltip>
|
||||
<el-table-column label="成员" min-width="300">
|
||||
<template slot-scope="scope">
|
||||
<div class="entity-cell">
|
||||
<el-avatar size="small" :src="getHeadImage(scope.row.wxid)" @error="() => true">
|
||||
@@ -205,7 +205,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="display_name" label="群昵称" min-width="180" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="display_name" label="群昵称" width="240"></el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="scope.row.status === 1 ? 'success' : 'info'">
|
||||
@@ -213,14 +213,14 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="latest_active_time" label="活跃时间" width="168" show-overflow-tooltip>
|
||||
<el-table-column prop="latest_active_time" label="活跃时间">
|
||||
<template slot-scope="scope">{% raw %}{{ scope.row.latest_active_time || '-' }}{% endraw %}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="activity_level" label="互动强度" width="110"></el-table-column>
|
||||
<el-table-column label="回复建议" min-width="260" show-overflow-tooltip>
|
||||
<el-table-column label="回复建议" min-width="220" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{% raw %}{{ scope.row.response_style_hint || '-' }}{% endraw %}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="后台摘要" width="110" align="center">
|
||||
<el-table-column label="后台摘要" width="130" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="primary" plain @click="openMemberContextDialog(scope.row)">查看</el-button>
|
||||
</template>
|
||||
@@ -390,7 +390,6 @@
|
||||
<div class="message-content">
|
||||
<div v-if="msg.type === 'text'" v-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 === 'emoji'"><img :src="msg.content" class="chat-emoji-preview"></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" v-text="msg.content.title"></a><p v-text="msg.content.description"></p></div>
|
||||
@@ -400,31 +399,12 @@
|
||||
{% endraw %}
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<div v-if="emojiPanelVisible" class="emoji-panel" v-loading="emojiAssetsLoading">
|
||||
<div class="emoji-panel-header">
|
||||
<span>后台表情资产</span>
|
||||
<el-button size="mini" type="text" @click="loadEmojiAssets">刷新</el-button>
|
||||
</div>
|
||||
<el-empty v-if="!emojiAssetsLoading && !emojiAssets.length" description="暂无可用表情资产"></el-empty>
|
||||
<div v-else class="emoji-grid">
|
||||
{% raw %}
|
||||
<div v-for="asset in emojiAssets" :key="asset.md5" class="emoji-card" @click="sendEmojiMessage(asset)">
|
||||
<img :src="asset.file_path" class="emoji-thumb">
|
||||
<div class="emoji-meta">
|
||||
<div class="emoji-meta-line">{{ asset.source_name || asset.source_wxid || '未知来源' }}</div>
|
||||
<div class="emoji-meta-line">{{ asset.source_chatroom_name || asset.source_chatroom_id || '全局资产' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endraw %}
|
||||
</div>
|
||||
</div>
|
||||
<el-input type="textarea" :rows="3" placeholder="请输入消息..." v-model="messageInput" @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="emojiPanelVisible ? 'warning' : 'primary'" @click="toggleEmojiPanel">表情</el-button>
|
||||
<el-button size="small" type="success" @click="sendTextMessage">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -471,7 +451,6 @@
|
||||
memberContextDialogVisible: false, memberContextLoading: false, memberContext: null, currentContextMember: {},
|
||||
memberContextEnabled: false,
|
||||
chatDialogVisible: false, currentChatUser: null, messageInput: '', chatMessages: [],
|
||||
emojiPanelVisible: false, emojiAssetsLoading: false, emojiAssets: [],
|
||||
linkDialogVisible: false,
|
||||
linkForm: { url: '', title: '', description: '' }
|
||||
};
|
||||
@@ -608,13 +587,7 @@
|
||||
this.$message.error('刷新成员交互摘要失败');
|
||||
}).finally(() => { this.memberContextLoading = false; });
|
||||
},
|
||||
openChatDialog(user) {
|
||||
this.currentChatUser = user;
|
||||
this.chatDialogVisible = true;
|
||||
this.chatMessages = [];
|
||||
this.emojiPanelVisible = false;
|
||||
this.loadEmojiAssets();
|
||||
},
|
||||
openChatDialog(user) { this.currentChatUser = user; this.chatDialogVisible = true; this.chatMessages = []; },
|
||||
async sendTextMessage() {
|
||||
if (!this.messageInput.trim()) return;
|
||||
try {
|
||||
@@ -634,56 +607,6 @@
|
||||
const formData = new FormData(); formData.append('file', options.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('发送视频失败'); }
|
||||
},
|
||||
toggleEmojiPanel() {
|
||||
this.emojiPanelVisible = !this.emojiPanelVisible;
|
||||
if (this.emojiPanelVisible && !this.emojiAssets.length) {
|
||||
this.loadEmojiAssets();
|
||||
}
|
||||
},
|
||||
async loadEmojiAssets() {
|
||||
this.emojiAssetsLoading = true;
|
||||
const roomid = this.currentChatUser && this.currentChatUser.wxid && this.currentChatUser.wxid.endsWith('@chatroom')
|
||||
? this.currentChatUser.wxid
|
||||
: '';
|
||||
try {
|
||||
const response = await axios.get('/contacts/api/emoji_assets', { params: { limit: 80, roomid } });
|
||||
if (response.data.success) {
|
||||
this.emojiAssets = response.data.data.assets || [];
|
||||
} else {
|
||||
this.$message.error('加载表情资产失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载表情资产失败:', error);
|
||||
this.$message.error('加载表情资产失败');
|
||||
} finally {
|
||||
this.emojiAssetsLoading = false;
|
||||
}
|
||||
},
|
||||
async sendEmojiMessage(asset) {
|
||||
if (!asset || !asset.md5 || !asset.total_length) {
|
||||
this.$message.warning('表情资产参数不完整');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await axios.post('/contacts/api/send_message', {
|
||||
wxid: this.currentChatUser.wxid,
|
||||
type: 'emoji',
|
||||
content: {
|
||||
md5: asset.md5,
|
||||
total_length: asset.total_length
|
||||
}
|
||||
});
|
||||
if (response.data.success) {
|
||||
this.chatMessages.push({ type: 'emoji', content: asset.file_path, isSelf: true, time: new Date().toLocaleTimeString() });
|
||||
this.$nextTick(() => { this.scrollToBottom(); });
|
||||
} else {
|
||||
this.$message.error(response.data.message || '发送表情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送表情失败:', error);
|
||||
this.$message.error('发送表情失败');
|
||||
}
|
||||
},
|
||||
showLinkDialog() { this.linkForm = { url: '', title: '', description: '' }; this.linkDialogVisible = true; },
|
||||
async sendLinkMessage() {
|
||||
if (!this.linkForm.url) { this.$message.warning('请输入链接'); return; }
|
||||
@@ -730,14 +653,6 @@
|
||||
.action-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.pagination-container { margin-top: 20px; text-align: right; }
|
||||
.group-members-section { margin-top: 20px; }
|
||||
.group-members-table .entity-cell { min-width: 0; }
|
||||
.group-members-table .entity-copy { min-width: 0; overflow: hidden; }
|
||||
.group-members-table .entity-title,
|
||||
.group-members-table .entity-subtitle {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.section-title {
|
||||
margin: 20px 0 15px 0; border-bottom: 1px solid rgba(148,163,184,0.12); padding-bottom: 10px;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
@@ -767,34 +682,5 @@
|
||||
.message-time { font-size: 12px; color: #94a3b8; margin-top: 5px; }
|
||||
.input-area { padding: 20px 0 0; }
|
||||
.toolbar { margin-top: 10px; display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.chat-emoji-preview { width: 96px; height: 96px; object-fit: contain; display: block; }
|
||||
.emoji-panel {
|
||||
margin-bottom: 12px; padding: 12px; border: 1px solid rgba(148,163,184,0.14);
|
||||
border-radius: 16px; background: rgba(248,250,252,0.86);
|
||||
}
|
||||
.emoji-panel-header {
|
||||
display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;
|
||||
color: #334155; font-size: 13px; font-weight: 600;
|
||||
}
|
||||
.emoji-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px;
|
||||
max-height: 240px; overflow-y: auto;
|
||||
}
|
||||
.emoji-card {
|
||||
padding: 10px; border-radius: 14px; background: #ffffff; border: 1px solid rgba(148,163,184,0.12);
|
||||
cursor: pointer; transition: all .2s ease;
|
||||
}
|
||||
.emoji-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
||||
border-color: rgba(79,70,229,0.24);
|
||||
}
|
||||
.emoji-thumb { width: 72px; height: 72px; display: block; margin: 0 auto 8px; object-fit: contain; }
|
||||
.emoji-meta { font-size: 11px; color: #64748b; line-height: 1.4; }
|
||||
.emoji-meta-line {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -92,12 +92,6 @@
|
||||
<img v-if="scope.row.message_thumb" :src="scope.row.message_thumb" class="message-thumb" @click="showVideo(scope.row)">
|
||||
</div>
|
||||
|
||||
<div v-else-if="isEmojiMessage(scope.row)" class="message-media-preview">
|
||||
<div class="message-media-label">【表情消息】</div>
|
||||
<img v-if="scope.row.image_path" :src="getImageUrl(scope.row.image_path)" class="message-thumb" @click="showImage(scope.row)">
|
||||
<div v-else class="message-text-preview is-muted">表情下载中或暂无预览</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="message-text-preview is-muted">
|
||||
{% raw %}{{ scope.row.content || `【消息类型: ${scope.row.message_type}】` }}{% endraw %}
|
||||
</div>
|
||||
@@ -133,10 +127,9 @@
|
||||
<el-descriptions-item label="消息类型">{% raw %}{{ getMessageTypeName(selectedMessage.message_type) }}{% endraw %}</el-descriptions-item>
|
||||
<el-descriptions-item label="内容">{% raw %}{{ selectedMessage.content }}{% endraw %}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="selectedMessage.message_type == 3 || selectedMessage.message_type == 43 || isEmojiMessage(selectedMessage)" label="媒体内容">
|
||||
<el-descriptions-item v-if="selectedMessage.message_type == 3 || selectedMessage.message_type == 43" label="媒体内容">
|
||||
<img v-if="selectedMessage.message_type == 3 && selectedMessage.image_path" :src="getImageUrl(selectedMessage.image_path)" style="max-width: 100%; border-radius: 16px;">
|
||||
<img v-else-if="selectedMessage.message_type == 3 && selectedMessage.message_thumb" :src="selectedMessage.message_thumb" style="max-width: 100%; border-radius: 16px;">
|
||||
<img v-else-if="isEmojiMessage(selectedMessage) && selectedMessage.image_path" :src="getImageUrl(selectedMessage.image_path)" style="max-width: 100%; border-radius: 16px;">
|
||||
<video v-if="selectedMessage.message_type == 43 && selectedMessage.attachment_url" :src="selectedMessage.attachment_url" controls style="max-width: 100%; border-radius: 16px;"></video>
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -320,18 +313,12 @@
|
||||
this.selectedMessage = message;
|
||||
this.detailDialogVisible = true;
|
||||
},
|
||||
isEmojiMessage(message) {
|
||||
if (!message) return false;
|
||||
return String(message.message_type) === '47' || String(message.message_type) === '1090519089';
|
||||
},
|
||||
getMessageTypeName(type) {
|
||||
const typeMap = {
|
||||
1: '文本消息',
|
||||
3: '图片消息',
|
||||
43: '视频消息',
|
||||
47: '动画表情',
|
||||
49: '链接消息',
|
||||
1090519089: '大表情'
|
||||
49: '链接消息'
|
||||
};
|
||||
return typeMap[type] || `未知类型(${type})`;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user