diff --git a/frontend/composables/useApi.js b/frontend/composables/useApi.js index 2e10888..b8df076 100644 --- a/frontend/composables/useApi.js +++ b/frontend/composables/useApi.js @@ -95,6 +95,18 @@ export const useApi = () => { return await request(url, { method: 'POST' }) } + const downloadChatEmoji = async (data = {}) => { + return await request('/chat/media/emoji/download', { + method: 'POST', + body: { + account: data.account || null, + md5: data.md5 || '', + emoji_url: data.emoji_url || '', + force: !!data.force + } + }) + } + // 获取图片解密密钥 const getMediaKeys = async (params = {}) => { const query = new URLSearchParams() @@ -135,6 +147,7 @@ export const useApi = () => { listChatSessions, listChatMessages, openChatMediaFolder, + downloadChatEmoji, getMediaKeys, saveMediaKeys, decryptAllMedia diff --git a/frontend/pages/chat.vue b/frontend/pages/chat.vue index db63e2d..0b18b5b 100644 --- a/frontend/pages/chat.vue +++ b/frontend/pages/chat.vue @@ -4,7 +4,7 @@
-
+
{{ message.content || '通话' }}
-
- 表情 +
+
{{ message.content }} @@ -295,10 +305,10 @@
- +
- +
@@ -696,7 +706,7 @@ const FileIconDoc = defineComponent({ const FileIconXls = defineComponent({ render() { return h('svg', { viewBox: '0 0 24 24', fill: 'none', class: 'text-green-600' }, [ - h('path', { d: 'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z', stroke: 'currentColor', 'stroke-width': '1.5', fill: 'none' }), + h('path', { d: 'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h6v6h6v10H6z', stroke: 'currentColor', 'stroke-width': '1.5', fill: 'none' }), h('path', { d: 'M14 2v6h6', stroke: 'currentColor', 'stroke-width': '1.5' }), h('text', { x: '6', y: '17', 'font-size': '5', fill: 'currentColor', 'font-weight': 'bold' }, 'XLS') ]) @@ -931,12 +941,33 @@ const normalizeMessage = (msg) => { const fallbackAvatar = (!isSent && !selectedContact.value?.isGroup) ? (selectedContact.value?.avatar || null) : null const mediaBase = process.client ? 'http://localhost:8000' : '' + const localEmojiUrl = msg.emojiMd5 ? `${mediaBase}/api/chat/media/emoji?account=${encodeURIComponent(selectedAccount.value || '')}&md5=${encodeURIComponent(msg.emojiMd5)}&username=${encodeURIComponent(selectedContact.value?.username || '')}` : '' const normalizedImageUrl = msg.imageUrl || (msg.imageMd5 ? `${mediaBase}/api/chat/media/image?account=${encodeURIComponent(selectedAccount.value || '')}&md5=${encodeURIComponent(msg.imageMd5)}&username=${encodeURIComponent(selectedContact.value?.username || '')}` : '') - const normalizedEmojiUrl = msg.emojiUrl || (msg.emojiMd5 ? `${mediaBase}/api/chat/media/emoji?account=${encodeURIComponent(selectedAccount.value || '')}&md5=${encodeURIComponent(msg.emojiMd5)}&username=${encodeURIComponent(selectedContact.value?.username || '')}` : '') + const normalizedEmojiUrl = msg.emojiUrl || localEmojiUrl const normalizedVideoThumbUrl = msg.videoThumbUrl || (msg.videoThumbMd5 ? `${mediaBase}/api/chat/media/video_thumb?account=${encodeURIComponent(selectedAccount.value || '')}&md5=${encodeURIComponent(msg.videoThumbMd5)}&username=${encodeURIComponent(selectedContact.value?.username || '')}` : '') const normalizedVideoUrl = msg.videoUrl || (msg.videoMd5 ? `${mediaBase}/api/chat/media/video?account=${encodeURIComponent(selectedAccount.value || '')}&md5=${encodeURIComponent(msg.videoMd5)}&username=${encodeURIComponent(selectedContact.value?.username || '')}` : '') const normalizedVoiceUrl = msg.voiceUrl || (msg.serverId ? `${mediaBase}/api/chat/media/voice?account=${encodeURIComponent(selectedAccount.value || '')}&server_id=${encodeURIComponent(String(msg.serverId))}` : '') + const remoteFromServer = ( + typeof msg.emojiRemoteUrl === 'string' + && /^https?:\/\//i.test(msg.emojiRemoteUrl) + && !/\/api\/chat\/media\/emoji\b/i.test(msg.emojiRemoteUrl) + && !/\blocalhost\b/i.test(msg.emojiRemoteUrl) + && !/\b127\.0\.0\.1\b/i.test(msg.emojiRemoteUrl) + ) ? msg.emojiRemoteUrl : '' + + const remoteFromEmojiUrl = ( + typeof msg.emojiUrl === 'string' + && /^https?:\/\//i.test(msg.emojiUrl) + && !/\/api\/chat\/media\/emoji\b/i.test(msg.emojiUrl) + && !/\blocalhost\b/i.test(msg.emojiUrl) + && !/\b127\.0\.0\.1\b/i.test(msg.emojiUrl) + ) ? msg.emojiUrl : '' + + const emojiRemoteUrl = remoteFromServer || remoteFromEmojiUrl + const emojiIsLocal = typeof normalizedEmojiUrl === 'string' && /\/api\/chat\/media\/emoji\b/i.test(normalizedEmojiUrl) + const emojiDownloaded = !!emojiRemoteUrl && !!emojiIsLocal + const replyText = String(msg.content || '').trim() let quoteContent = String(msg.quoteContent || '') const qcTrim = quoteContent.trim() @@ -971,6 +1002,9 @@ const normalizeMessage = (msg) => { imageMd5: msg.imageMd5 || '', emojiMd5: msg.emojiMd5 || '', emojiUrl: normalizedEmojiUrl || '', + emojiLocalUrl: localEmojiUrl || '', + emojiRemoteUrl, + _emojiDownloaded: !!emojiDownloaded, thumbUrl: msg.thumbUrl || '', imageUrl: normalizedImageUrl || '', videoMd5: msg.videoMd5 || '', @@ -996,6 +1030,47 @@ const normalizeMessage = (msg) => { } } +const shouldShowEmojiDownload = (message) => { + if (!message?.emojiMd5) return false + const u = String(message?.emojiRemoteUrl || '').trim() + if (!u) return false + if (!/^https?:\/\//i.test(u)) return false + return true +} + +const onEmojiDownloadClick = async (message) => { + if (!process.client) return + if (!message?.emojiMd5) return + if (!selectedAccount.value) return + + const emojiUrl = String(message?.emojiRemoteUrl || '').trim() + if (!emojiUrl) { + window.alert('该表情没有可用的下载地址') + return + } + + if (message._emojiDownloading) return + message._emojiDownloading = true + + try { + const api = useApi() + await api.downloadChatEmoji({ + account: selectedAccount.value, + md5: message.emojiMd5, + emoji_url: emojiUrl, + force: false + }) + message._emojiDownloaded = true + if (message.emojiLocalUrl) { + message.emojiUrl = message.emojiLocalUrl + } + } catch (e) { + window.alert(e?.message || '下载失败') + } finally { + message._emojiDownloading = false + } +} + const onGlobalClick = () => { if (contextMenu.value.visible) closeContextMenu() } @@ -1137,6 +1212,7 @@ const LinkCard = defineComponent({ ) } }) +