改动结果:

聊天窗口工具栏新增了“表情”按钮,打开表情库弹窗。
表情库会从历史“已下载落盘的表情消息”里自动聚合。
选中后直接通过 send_emoji_message(wxid, md5, total_length) 发原生表情,不是当普通图片发。
仍保持你现在的发送通道和聊天刷新逻辑。
主要改动文件:

后端接口与发送支持:contacts.py
表情资源查询:message_storage.py
前端表情面板与发送交互:contacts_management.html
新增接口:

GET /contacts/api/emojis:返回聚合后的表情库(md5、total_length、预览图)。
POST /contacts/api/send_message 新增 type=emoji。
我也做了 Python 语法检查,相关后端文件都通过了。
你可以直接在聊天弹窗里点“表情”试一下。如果表情库为空,通常是该群还没落盘到 image_path,让媒体下载功能先抓几条表情就会出现。
This commit is contained in:
liuwei
2026-04-15 11:21:32 +08:00
parent 96f50d929b
commit 47f8bd5717
3 changed files with 191 additions and 1 deletions
@@ -679,6 +679,7 @@
<el-upload class="upload-demo" action="#" :http-request="uploadImage" :show-file-list="false" accept="image/*">
<el-button size="small" type="primary" plain icon="el-icon-picture-outline">图片</el-button>
</el-upload>
<el-button size="small" type="primary" plain icon="el-icon-star-off" @click="openEmojiDialog">表情</el-button>
<el-upload class="upload-demo" action="#" :http-request="uploadVoice" :show-file-list="false" accept=".mp3,.wav,audio/*">
<el-button size="small" type="primary" plain icon="el-icon-microphone">语音</el-button>
</el-upload>
@@ -703,6 +704,25 @@
<el-button type="primary" @click="sendLinkMessage">发送</el-button>
</span>
</el-dialog>
<el-dialog title="表情库" :visible.sync="emojiDialogVisible" width="52%">
<div class="emoji-toolbar">
<el-input v-model="emojiKeyword" clearable placeholder="搜索 md5..." size="small"></el-input>
<el-button size="small" icon="el-icon-refresh" :loading="emojiLibraryLoading" @click="loadEmojiLibrary">刷新</el-button>
</div>
<div class="emoji-grid" v-loading="emojiLibraryLoading">
{% raw %}
<div v-if="!filteredEmojiLibrary.length" class="emoji-empty">暂无可用表情,先在群里让媒体下载插件抓取几条表情。</div>
<div v-for="item in filteredEmojiLibrary" :key="item.md5" class="emoji-card">
<img class="emoji-thumb" :src="getChatMediaUrl(item.preview_url)" />
<div class="emoji-md5">{{ item.md5 }}</div>
<div class="emoji-actions">
<el-button type="primary" size="mini" @click="sendEmojiItem(item)">发送</el-button>
</div>
</div>
{% endraw %}
</div>
</el-dialog>
</div>
{% endblock %}
@@ -739,7 +759,11 @@
chatDialogVisible: false, currentChatUser: null, messageInput: '', chatMessages: [], chatLoading: false, chatSending: false,
chatType: 'personal', chatHistoryTip: '最近 20 条消息',
linkDialogVisible: false,
linkForm: { url: '', title: '', description: '' }
linkForm: { url: '', title: '', description: '' },
emojiDialogVisible: false,
emojiLibraryLoading: false,
emojiLibrary: [],
emojiKeyword: ''
};
},
computed: {
@@ -781,6 +805,11 @@
{ label: '插件调用', value: overview.plugin_call_count_30d || 0, note: '近30天插件总触发次数' },
{ label: '插件种类', value: overview.plugin_count_30d || 0, note: '本群真实使用到的插件数' }
];
},
filteredEmojiLibrary() {
const keyword = (this.emojiKeyword || '').trim().toLowerCase();
if (!keyword) return this.emojiLibrary;
return this.emojiLibrary.filter(item => (item.md5 || '').toLowerCase().includes(keyword));
}
},
mounted() {
@@ -1319,6 +1348,50 @@
this.linkDialogVisible = false;
}
},
openEmojiDialog() {
if (!this.currentChatUser || !this.currentChatUser.wxid) return;
this.emojiDialogVisible = true;
this.loadEmojiLibrary();
},
async loadEmojiLibrary() {
this.emojiLibraryLoading = true;
try {
const response = await axios.get('/contacts/api/emojis', { params: { limit: 300 } });
if (response.data.success) {
const list = (response.data.data && response.data.data.emojis) || [];
this.emojiLibrary = Array.isArray(list) ? list : [];
} else {
this.$message.error(response.data.message || '加载表情库失败');
}
} catch (error) {
console.error('加载表情库失败:', error);
this.$message.error('加载表情库失败');
} finally {
this.emojiLibraryLoading = false;
}
},
async sendEmojiItem(item) {
if (!this.currentChatUser || !this.currentChatUser.wxid) return;
const md5 = item && item.md5;
const totalLength = item && item.total_length;
if (!md5 || !totalLength) {
this.$message.error('该表情缺少发送参数');
return;
}
const localMessage = this.appendLocalChatMessage({
displayType: 'image',
content: '[表情]',
mediaUrl: this.getChatMediaUrl(item.preview_url || '')
});
await this.sendChatPayload({
wxid: this.currentChatUser.wxid,
type: 'emoji',
content: {
md5: md5,
total_length: totalLength
}
}, '表情消息已提交到 iPad 通道', localMessage.messageId);
},
handleChatInputKeydown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
@@ -1493,6 +1566,19 @@
.message-self .message-sender, .message-self .message-time, .message-self .message-link-title { color: #ffffff; }
.input-area { padding: 20px 0 0; }
.toolbar { margin-top: 12px; display: flex; gap: 10px; flex-wrap: wrap; }
.emoji-toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
.emoji-grid {
min-height: 280px; max-height: 520px; overflow-y: auto;
display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px;
}
.emoji-card {
border: 1px solid rgba(148,163,184,0.16); border-radius: 12px; padding: 8px;
display: flex; flex-direction: column; gap: 8px; align-items: center; background: #fff;
}
.emoji-thumb { width: 72px; height: 72px; object-fit: contain; border-radius: 8px; background: rgba(148,163,184,0.08); }
.emoji-md5 { font-size: 11px; color: #64748b; word-break: break-all; text-align: center; min-height: 30px; }
.emoji-actions { width: 100%; display: flex; justify-content: center; }
.emoji-empty { color: #94a3b8; padding: 12px; }
@media (max-width: 1200px) {
.diagnosis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}