diff --git a/frontend/components/ApiStatus.vue b/frontend/components/ApiStatus.vue index d866a1e..5b68826 100644 --- a/frontend/components/ApiStatus.vue +++ b/frontend/components/ApiStatus.vue @@ -8,7 +8,7 @@

API连接问题

{{ appStore.apiMessage || '无法连接到后端服务' }}

-

请确保后端服务正在运行 (端口: 8000)

+

请确保后端服务正在运行

@@ -18,4 +18,4 @@ import { useAppStore } from '~/stores/app' const appStore = useAppStore() - \ No newline at end of file + diff --git a/frontend/components/EditedMessagePreview.vue b/frontend/components/EditedMessagePreview.vue index e698a38..50460d7 100644 --- a/frontend/components/EditedMessagePreview.vue +++ b/frontend/components/EditedMessagePreview.vue @@ -101,13 +101,13 @@ const props = defineProps({ message: { type: Object, default: null }, }) -const mediaBase = process.client ? 'http://localhost:8000' : '' +const apiBase = useApiBase() const normalizeMaybeUrl = (u) => { const raw = String(u || '').trim() if (!raw) return '' if (/^https?:\/\//i.test(raw) || /^blob:/i.test(raw) || /^data:/i.test(raw)) return raw - if (/^\/api\//i.test(raw)) return `${mediaBase}${raw}` + if (/^\/api\//i.test(raw)) return `${apiBase}${raw.slice(4)}` return raw } diff --git a/frontend/components/wrapped/cards/Card00GlobalOverview.vue b/frontend/components/wrapped/cards/Card00GlobalOverview.vue index c39007e..d9c483b 100644 --- a/frontend/components/wrapped/cards/Card00GlobalOverview.vue +++ b/frontend/components/wrapped/cards/Card00GlobalOverview.vue @@ -137,8 +137,7 @@ const topGroup = computed(() => { return o && typeof o === 'object' && typeof o.displayName === 'string' ? o : null }) -// Keep the same behavior as the chat page: media (including avatars) comes from backend :8000 in dev. -const mediaBase = process.client ? 'http://localhost:8000' : '' +const apiBase = useApiBase() const resolveMediaUrl = (value) => { const raw = String(value || '').trim() if (!raw) return '' @@ -147,13 +146,13 @@ const resolveMediaUrl = (value) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw } - // Most backend fields are like "/api/...", so just prefix. - return `${mediaBase}${raw.startsWith('/') ? '' : '/'}${raw}` + if (/^\/api\//i.test(raw)) return `${apiBase}${raw.slice(4)}` + return raw.startsWith('/') ? raw : `/${raw}` } const topContactAvatarUrl = computed(() => { diff --git a/frontend/components/wrapped/cards/Card03ReplySpeed.vue b/frontend/components/wrapped/cards/Card03ReplySpeed.vue index 50b00c9..4a06a62 100644 --- a/frontend/components/wrapped/cards/Card03ReplySpeed.vue +++ b/frontend/components/wrapped/cards/Card03ReplySpeed.vue @@ -308,7 +308,7 @@ const indexBuild = computed(() => { }) // Media URL resolving (same behavior as other wrapped components) -const mediaBase = process.client ? 'http://localhost:8000' : '' +const apiBase = useApiBase() const resolveMediaUrl = (value) => { const raw = String(value || '').trim() if (!raw) return '' @@ -316,12 +316,13 @@ const resolveMediaUrl = (value) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw } - return `${mediaBase}${raw.startsWith('/') ? '' : '/'}${raw}` + if (/^\/api\//i.test(raw)) return `${apiBase}${raw.slice(4)}` + return raw.startsWith('/') ? raw : `/${raw}` } const avatarFallback = (name) => { diff --git a/frontend/components/wrapped/cards/Card04EmojiUniverse.vue b/frontend/components/wrapped/cards/Card04EmojiUniverse.vue index f2a16a2..dc940a5 100644 --- a/frontend/components/wrapped/cards/Card04EmojiUniverse.vue +++ b/frontend/components/wrapped/cards/Card04EmojiUniverse.vue @@ -270,7 +270,7 @@ const props = defineProps({ const nfInt = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }) const formatInt = (n) => nfInt.format(Math.round(Number(n) || 0)) -const mediaBase = process.client ? 'http://localhost:8000' : '' +const apiBase = useApiBase() const resolveMediaUrl = (value, opts = { backend: false }) => { const raw = String(value || '').trim() if (!raw) return '' @@ -278,13 +278,15 @@ const resolveMediaUrl = (value, opts = { backend: false }) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw } - if (opts.backend || raw.startsWith('/api/')) { - return `${mediaBase}${raw.startsWith('/') ? '' : '/'}${raw}` + if (/^\/api\//i.test(raw)) return `${apiBase}${raw.slice(4)}` + if (opts.backend) { + const origin = apiBase.endsWith('/api') ? apiBase.slice(0, -4) : apiBase + return `${origin}${raw.startsWith('/') ? '' : '/'}${raw}` } return raw.startsWith('/') ? raw : `/${raw}` } diff --git a/frontend/components/wrapped/cards/Card04MonthlyBestFriendsWall.vue b/frontend/components/wrapped/cards/Card04MonthlyBestFriendsWall.vue index 3810632..e30a888 100644 --- a/frontend/components/wrapped/cards/Card04MonthlyBestFriendsWall.vue +++ b/frontend/components/wrapped/cards/Card04MonthlyBestFriendsWall.vue @@ -166,7 +166,7 @@ const formatScore = (n) => { } const clampPct = (n) => Math.max(0, Math.min(100, Math.round(Number(n || 0) * 100))) -const mediaBase = process.client ? 'http://localhost:8000' : '' +const apiBase = useApiBase() const resolveMediaUrl = (value) => { const raw = String(value || '').trim() if (!raw) return '' @@ -174,12 +174,13 @@ const resolveMediaUrl = (value) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw } - return `${mediaBase}${raw.startsWith('/') ? '' : '/'}${raw}` + if (/^\/api\//i.test(raw)) return `${apiBase}${raw.slice(4)}` + return raw.startsWith('/') ? raw : `/${raw}` } const avatarFallback = (name) => { diff --git a/frontend/components/wrapped/cards/Card07BentoSummary.vue b/frontend/components/wrapped/cards/Card07BentoSummary.vue index 381d8e9..d399942 100644 --- a/frontend/components/wrapped/cards/Card07BentoSummary.vue +++ b/frontend/components/wrapped/cards/Card07BentoSummary.vue @@ -944,6 +944,8 @@ const formatDurationZh = (seconds) => { return h > 0 ? `${d}天${h}小时` : `${d}天` } +const apiBase = useApiBase() + const resolveMediaUrl = (value, opts = { backend: false }) => { const raw = String(value || '').trim() if (!raw) return '' @@ -952,13 +954,12 @@ const resolveMediaUrl = (value, opts = { backend: false }) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - // Keep same-origin `/api` so Nuxt devProxy / backend-mounted UI both work. - return `/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw } - // Keep `/api/...` as same-origin (avoid hardcoding backend host like `localhost:8000`). + if (/^\/api\//i.test(raw)) return `${apiBase}${raw.slice(4)}` return raw.startsWith('/') ? raw : `/${raw}` } diff --git a/frontend/components/wrapped/visualizations/ChatReplayAnimation.vue b/frontend/components/wrapped/visualizations/ChatReplayAnimation.vue index 3d88b7e..3ded96b 100644 --- a/frontend/components/wrapped/visualizations/ChatReplayAnimation.vue +++ b/frontend/components/wrapped/visualizations/ChatReplayAnimation.vue @@ -78,8 +78,7 @@ const onAvatarError = () => { avatarOk.value = false } const displayNameShown = computed(() => String(props.displayName || props.maskedName || '').trim()) -// Keep the same behavior as the chat page: media (including avatars) comes from backend :8000 in dev. -const mediaBase = process.client ? 'http://localhost:8000' : '' +const apiBase = useApiBase() const resolveMediaUrl = (value) => { const raw = String(value || '').trim() if (!raw) return '' @@ -88,13 +87,13 @@ const resolveMediaUrl = (value) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw } - // Most backend fields are like "/api/...", so just prefix. - return `${mediaBase}${raw.startsWith('/') ? '' : '/'}${raw}` + if (/^\/api\//i.test(raw)) return `${apiBase}${raw.slice(4)}` + return raw.startsWith('/') ? raw : `/${raw}` } const resolvedAvatarUrl = computed(() => resolveMediaUrl(props.avatarUrl)) diff --git a/frontend/composables/useApi.js b/frontend/composables/useApi.js index 9ae1de6..416e5e8 100644 --- a/frontend/composables/useApi.js +++ b/frontend/composables/useApi.js @@ -1,15 +1,10 @@ // API请求组合式函数 export const useApi = () => { - const config = useRuntimeConfig() + const baseURL = useApiBase() // 基础请求函数 const request = async (url, options = {}) => { try { - // Default to same-origin `/api` so Nuxt devProxy / backend-mounted UI both work. - // Override via `NUXT_PUBLIC_API_BASE`, e.g. `http://127.0.0.1:8000/api`. - const apiBase = String(config?.public?.apiBase || '').trim() - const baseURL = (apiBase ? apiBase : '/api').replace(/\/$/, '') - const response = await $fetch(url, { baseURL, ...options, diff --git a/frontend/pages/chat/[[username]].vue b/frontend/pages/chat/[[username]].vue index 23c7f6d..7e11745 100644 --- a/frontend/pages/chat/[[username]].vue +++ b/frontend/pages/chat/[[username]].vue @@ -3630,8 +3630,8 @@ const startExportPolling = (exportId) => { if (!exportId) return if (process.client && typeof window !== 'undefined' && typeof EventSource !== 'undefined') { - const base = 'http://localhost:8000' - const url = `${base}/api/chat/exports/${encodeURIComponent(String(exportId))}/events` + const apiBase = useApiBase() + const url = `${apiBase}/chat/exports/${encodeURIComponent(String(exportId))}/events` try { exportEventSource = new EventSource(url) exportEventSource.onmessage = (ev) => { @@ -3889,8 +3889,8 @@ watch( ) const getExportDownloadUrl = (exportId) => { - const base = process.client ? 'http://localhost:8000' : '' - return `${base}/api/chat/exports/${encodeURIComponent(String(exportId || ''))}/download` + const apiBase = useApiBase() + return `${apiBase}/chat/exports/${encodeURIComponent(String(exportId || ''))}/download` } const startChatExport = async () => { @@ -6089,7 +6089,7 @@ const normalizeMessage = (msg) => { const sender = isSent ? '我' : (msg.senderDisplayName || msg.senderUsername || selectedContact.value?.name || '') const fallbackAvatar = (!isSent && !selectedContact.value?.isGroup) ? (selectedContact.value?.avatar || null) : null - const mediaBase = process.client ? 'http://localhost:8000' : '' + const apiBase = useApiBase() const normalizeMaybeUrl = (u) => (typeof u === 'string' ? u.trim() : '') const isUsableMediaUrl = (u) => { const v = normalizeMaybeUrl(u) @@ -6121,7 +6121,7 @@ const normalizeMessage = (msg) => { try { const host = new URL(u).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(u)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(u)}` } } catch {} return u @@ -6129,15 +6129,15 @@ const normalizeMessage = (msg) => { const fromUsername = String(msg.fromUsername || '').trim() const fromAvatar = fromUsername - ? `${mediaBase}/api/chat/avatar?account=${encodeURIComponent(selectedAccount.value || '')}&username=${encodeURIComponent(fromUsername)}` + ? `${apiBase}/chat/avatar?account=${encodeURIComponent(selectedAccount.value || '')}&username=${encodeURIComponent(fromUsername)}` : (() => { // App/web link shares may not provide `fromUsername` (sourceusername), so we don't have a WeChat avatar. // Fall back to a best-effort website favicon fetched via backend. const href = String(msg.url || '').trim() - return href ? `${mediaBase}/api/chat/media/favicon?url=${encodeURIComponent(href)}` : '' + return href ? `${apiBase}/chat/media/favicon?url=${encodeURIComponent(href)}` : '' })() - const localEmojiUrl = msg.emojiMd5 ? `${mediaBase}/api/chat/media/emoji?account=${encodeURIComponent(selectedAccount.value || '')}&md5=${encodeURIComponent(msg.emojiMd5)}&username=${encodeURIComponent(selectedContact.value?.username || '')}` : '' + const localEmojiUrl = msg.emojiMd5 ? `${apiBase}/chat/media/emoji?account=${encodeURIComponent(selectedAccount.value || '')}&md5=${encodeURIComponent(msg.emojiMd5)}&username=${encodeURIComponent(selectedContact.value?.username || '')}` : '' const localImageUrl = (() => { if (!msg.imageMd5 && !msg.imageFileId) return '' const parts = [ @@ -6146,7 +6146,7 @@ const normalizeMessage = (msg) => { msg.imageFileId ? `file_id=${encodeURIComponent(msg.imageFileId)}` : '', `username=${encodeURIComponent(selectedContact.value?.username || '')}`, ].filter(Boolean) - return `${mediaBase}/api/chat/media/image?${parts.join('&')}` + return `${apiBase}/chat/media/image?${parts.join('&')}` })() const normalizedImageUrl = (() => { const cur = (isUsableMediaUrl(msg.imageUrl) ? normalizeMaybeUrl(msg.imageUrl) : '') @@ -6165,7 +6165,7 @@ const normalizeMessage = (msg) => { msg.videoThumbFileId ? `file_id=${encodeURIComponent(msg.videoThumbFileId)}` : '', `username=${encodeURIComponent(selectedContact.value?.username || '')}`, ].filter(Boolean) - return `${mediaBase}/api/chat/media/video_thumb?${parts.join('&')}` + return `${apiBase}/chat/media/video_thumb?${parts.join('&')}` })() const localVideoUrl = (() => { @@ -6176,7 +6176,7 @@ const normalizeMessage = (msg) => { msg.videoFileId ? `file_id=${encodeURIComponent(msg.videoFileId)}` : '', `username=${encodeURIComponent(selectedContact.value?.username || '')}`, ].filter(Boolean) - return `${mediaBase}/api/chat/media/video?${parts.join('&')}` + return `${apiBase}/chat/media/video?${parts.join('&')}` })() const normalizedVideoThumbUrl = (isUsableMediaUrl(msg.videoThumbUrl) ? normalizeMaybeUrl(msg.videoThumbUrl) : '') || localVideoThumbUrl @@ -6186,7 +6186,7 @@ const normalizeMessage = (msg) => { if (msg.voiceUrl) return msg.voiceUrl if (!serverIdStr) return '' if (String(msg.renderType || '') !== 'voice') return '' - return `${mediaBase}/api/chat/media/voice?account=${encodeURIComponent(selectedAccount.value || '')}&server_id=${encodeURIComponent(serverIdStr)}` + return `${apiBase}/chat/media/voice?account=${encodeURIComponent(selectedAccount.value || '')}&server_id=${encodeURIComponent(serverIdStr)}` })() const remoteFromServer = ( @@ -6228,7 +6228,7 @@ const normalizeMessage = (msg) => { const quoteServerIdStr = String(msg.quoteServerId || '').trim() const quoteTypeStr = String(msg.quoteType || '').trim() const quoteVoiceUrl = quoteServerIdStr - ? `${mediaBase}/api/chat/media/voice?account=${encodeURIComponent(selectedAccount.value || '')}&server_id=${encodeURIComponent(quoteServerIdStr)}` + ? `${apiBase}/chat/media/voice?account=${encodeURIComponent(selectedAccount.value || '')}&server_id=${encodeURIComponent(quoteServerIdStr)}` : '' const quoteImageUrl = (() => { if (!quoteServerIdStr) return '' @@ -6239,7 +6239,7 @@ const normalizeMessage = (msg) => { `server_id=${encodeURIComponent(quoteServerIdStr)}`, convUsername ? `username=${encodeURIComponent(convUsername)}` : '' ].filter(Boolean) - return parts.length ? `${mediaBase}/api/chat/media/image?${parts.join('&')}` : '' + return parts.length ? `${apiBase}/chat/media/image?${parts.join('&')}` : '' })() const quoteThumbUrl = (() => { const raw = isUsableMediaUrl(msg.quoteThumbUrl) ? normalizeMaybeUrl(msg.quoteThumbUrl) : '' @@ -6249,7 +6249,7 @@ const normalizeMessage = (msg) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw @@ -6744,7 +6744,7 @@ const formatChatHistoryVideoDuration = (value) => { } const normalizeChatHistoryRecordItem = (rec) => { - const mediaBase = process.client ? 'http://localhost:8000' : '' + const apiBase = useApiBase() const account = encodeURIComponent(selectedAccount.value || '') const username = encodeURIComponent(selectedContact.value?.username || '') @@ -6768,7 +6768,7 @@ const normalizeChatHistoryRecordItem = (rec) => { })() if (fileId) { previewCandidates.push( - `${mediaBase}/api/chat/media/image?account=${account}&file_id=${encodeURIComponent(fileId)}&username=${username}` + `${apiBase}/chat/media/image?account=${account}&file_id=${encodeURIComponent(fileId)}&username=${username}` ) } @@ -6782,7 +6782,7 @@ const normalizeChatHistoryRecordItem = (rec) => { srcServerId ? `server_id=${encodeURIComponent(srcServerId)}` : '', `username=${username}` ].filter(Boolean) - previewCandidates.push(`${mediaBase}/api/chat/media/image?${previewParts.join('&')}`) + previewCandidates.push(`${apiBase}/chat/media/image?${previewParts.join('&')}`) } out._linkPreviewCandidates = previewCandidates @@ -6793,8 +6793,8 @@ const normalizeChatHistoryRecordItem = (rec) => { const fromUsername = String(out.fromUsername || '').trim() out.fromUsername = fromUsername out.fromAvatar = fromUsername - ? `${mediaBase}/api/chat/avatar?account=${account}&username=${encodeURIComponent(fromUsername)}` - : (linkUrl ? `${mediaBase}/api/chat/media/favicon?url=${encodeURIComponent(linkUrl)}` : '') + ? `${apiBase}/chat/avatar?account=${account}&username=${encodeURIComponent(fromUsername)}` + : (linkUrl ? `${apiBase}/chat/media/favicon?url=${encodeURIComponent(linkUrl)}` : '') out._fromAvatarLast = out.fromAvatar out._fromAvatarImgOk = false out._fromAvatarImgError = false @@ -6804,17 +6804,17 @@ const normalizeChatHistoryRecordItem = (rec) => { out.videoDuration = String(out.duration || '').trim() const thumbCandidates = [] if (out.videoMd5) { - thumbCandidates.push(`${mediaBase}/api/chat/media/video_thumb?account=${account}&md5=${encodeURIComponent(out.videoMd5)}&username=${username}`) + thumbCandidates.push(`${apiBase}/chat/media/video_thumb?account=${account}&md5=${encodeURIComponent(out.videoMd5)}&username=${username}`) } if (out.videoThumbMd5 && out.videoThumbMd5 !== out.videoMd5) { - thumbCandidates.push(`${mediaBase}/api/chat/media/video_thumb?account=${account}&md5=${encodeURIComponent(out.videoThumbMd5)}&username=${username}`) + thumbCandidates.push(`${apiBase}/chat/media/video_thumb?account=${account}&md5=${encodeURIComponent(out.videoThumbMd5)}&username=${username}`) } out._videoThumbCandidates = thumbCandidates out._videoThumbCandidateIndex = 0 out._videoThumbError = false out.videoThumbUrl = thumbCandidates[0] || '' out.videoUrl = out.videoMd5 - ? `${mediaBase}/api/chat/media/video?account=${account}&md5=${encodeURIComponent(out.videoMd5)}&username=${username}` + ? `${apiBase}/chat/media/video?account=${account}&md5=${encodeURIComponent(out.videoMd5)}&username=${username}` : '' if (!out.content || /^\[.+\]$/.test(String(out.content || '').trim())) out.content = '[视频]' } else if (out.renderType === 'emoji') { @@ -6823,7 +6823,7 @@ const normalizeChatHistoryRecordItem = (rec) => { const remoteAesKey = String(out.aeskey || '').trim() out.emojiRemoteUrl = remoteEmojiUrl out.emojiUrl = out.emojiMd5 - ? `${mediaBase}/api/chat/media/emoji?account=${account}&md5=${encodeURIComponent(out.emojiMd5)}&username=${username}${remoteEmojiUrl ? `&emoji_url=${encodeURIComponent(remoteEmojiUrl)}` : ''}${remoteAesKey ? `&aes_key=${encodeURIComponent(remoteAesKey)}` : ''}` + ? `${apiBase}/chat/media/emoji?account=${account}&md5=${encodeURIComponent(out.emojiMd5)}&username=${username}${remoteEmojiUrl ? `&emoji_url=${encodeURIComponent(remoteEmojiUrl)}` : ''}${remoteAesKey ? `&aes_key=${encodeURIComponent(remoteAesKey)}` : ''}` : '' if (!out.content || /^\[.+\]$/.test(String(out.content || '').trim())) out.content = '[表情]' } else if (out.renderType === 'image') { @@ -6835,7 +6835,7 @@ const normalizeChatHistoryRecordItem = (rec) => { srcServerId ? `server_id=${encodeURIComponent(srcServerId)}` : '', `username=${username}` ].filter(Boolean) - out.imageUrl = imgParts.length ? `${mediaBase}/api/chat/media/image?${imgParts.join('&')}` : '' + out.imageUrl = imgParts.length ? `${apiBase}/chat/media/image?${imgParts.join('&')}` : '' if (!out.content || /^\[.+\]$/.test(String(out.content || '').trim())) out.content = '[图片]' } @@ -7190,7 +7190,7 @@ const resolveChatHistoryLinkRecord = async (rec) => { const content = String(resp.content || '').trim() const url = String(resp.url || '').trim() const from = String(resp.from || '').trim() - const mediaBase = process.client ? 'http://localhost:8000' : '' + const apiBase = useApiBase() const normalizePreviewUrl = (u) => { const raw = String(u || '').trim() if (!raw) return '' @@ -7199,7 +7199,7 @@ const resolveChatHistoryLinkRecord = async (rec) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw @@ -7214,8 +7214,8 @@ const resolveChatHistoryLinkRecord = async (rec) => { const fromUsername = String(resp.fromUsername || '').trim() if (fromUsername) rec.fromUsername = fromUsername const fromAvatarUrl = fromUsername - ? `${mediaBase}/api/chat/avatar?account=${encodeURIComponent(selectedAccount.value || '')}&username=${encodeURIComponent(fromUsername)}` - : (url ? `${mediaBase}/api/chat/media/favicon?url=${encodeURIComponent(url)}` : '') + ? `${apiBase}/chat/avatar?account=${encodeURIComponent(selectedAccount.value || '')}&username=${encodeURIComponent(fromUsername)}` + : (url ? `${apiBase}/chat/media/favicon?url=${encodeURIComponent(url)}` : '') if (fromAvatarUrl) { const last = String(rec._fromAvatarLast || '').trim() rec.fromAvatar = fromAvatarUrl diff --git a/frontend/pages/decrypt.vue b/frontend/pages/decrypt.vue index 4d60b64..ffe0c49 100644 --- a/frontend/pages/decrypt.vue +++ b/frontend/pages/decrypt.vue @@ -775,7 +775,8 @@ const handleDecrypt = async () => { const params = new URLSearchParams() params.set('key', formData.key) params.set('db_storage_path', formData.db_storage_path) - const url = `http://localhost:8000/api/decrypt_stream?${params.toString()}` + const apiBase = useApiBase() + const url = `${apiBase}/decrypt_stream?${params.toString()}` dbDecryptProgress.message = '连接中...' const eventSource = new EventSource(url) @@ -904,7 +905,8 @@ const decryptAllImages = async () => { if (mediaAccount.value) params.set('account', mediaAccount.value) if (mediaKeys.xor_key) params.set('xor_key', mediaKeys.xor_key) if (mediaKeys.aes_key) params.set('aes_key', mediaKeys.aes_key) - const url = `http://localhost:8000/api/media/decrypt_all_stream?${params.toString()}` + const apiBase = useApiBase() + const url = `${apiBase}/media/decrypt_all_stream?${params.toString()}` // 使用EventSource接收SSE const eventSource = new EventSource(url) diff --git a/frontend/pages/edits/[[username]].vue b/frontend/pages/edits/[[username]].vue index 2561172..144c4c0 100644 --- a/frontend/pages/edits/[[username]].vue +++ b/frontend/pages/edits/[[username]].vue @@ -257,13 +257,13 @@ watch(selectedAccount, async () => { await loadSessions() }) -const mediaBase = process.client ? 'http://localhost:8000' : '' +const apiBase = useApiBase() const normalizeMaybeUrl = (u) => { const raw = String(u || '').trim() if (!raw) return '' if (/^https?:\/\//i.test(raw) || /^blob:/i.test(raw) || /^data:/i.test(raw)) return raw - if (/^\/api\//i.test(raw)) return `${mediaBase}${raw}` + if (/^\/api\//i.test(raw)) return `${apiBase}${raw.slice(4)}` return raw } diff --git a/frontend/pages/sns.vue b/frontend/pages/sns.vue index ae90ebe..3c5d121 100644 --- a/frontend/pages/sns.vue +++ b/frontend/pages/sns.vue @@ -794,7 +794,7 @@ const filteredSnsUsers = computed(() => { const pageSize = 20 -const mediaBase = process.client ? 'http://localhost:8000' : '' +const apiBase = useApiBase() // 朋友圈导出(HTML 离线 ZIP) const exportJob = ref(null) @@ -835,8 +835,7 @@ const startSnsExportPolling = (exportId) => { if (!exportId) return if (process.client && typeof window !== 'undefined' && typeof EventSource !== 'undefined') { - const base = 'http://localhost:8000' - const url = `${base}/api/sns/exports/${encodeURIComponent(String(exportId))}/events` + const url = `${apiBase}/sns/exports/${encodeURIComponent(String(exportId))}/events` try { exportEventSource = new EventSource(url) exportEventSource.onmessage = (ev) => { @@ -867,8 +866,7 @@ const downloadSnsExport = (exportId) => { if (!process.client) return const id = String(exportId || '').trim() if (!id) return - const base = 'http://localhost:8000' - const url = `${base}/api/sns/exports/${encodeURIComponent(id)}/download` + const url = `${apiBase}/sns/exports/${encodeURIComponent(id)}/download` window.open(url, '_blank', 'noopener,noreferrer') } @@ -1109,7 +1107,7 @@ const selfInfo = ref({ wxid: '', nickname: '' }) const loadSelfInfo = async () => { if (!selectedAccount.value) return try { - const resp = await $fetch(`${mediaBase}/api/sns/self_info?account=${encodeURIComponent(selectedAccount.value)}`) + const resp = await $fetch(`${apiBase}/sns/self_info?account=${encodeURIComponent(selectedAccount.value)}`) if (resp && resp.wxid) { selfInfo.value = resp } @@ -1145,7 +1143,7 @@ const selectSnsUser = async (username) => { const getArticleThumbProxyUrl = (contentUrl) => { const u = String(contentUrl || '').trim() if (!u) return '' - return `${mediaBase}/api/sns/article_thumb?url=${encodeURIComponent(u)}` + return `${apiBase}/sns/article_thumb?url=${encodeURIComponent(u)}` } const guessOfficialAccountNameFromTitle = (title) => { @@ -1443,7 +1441,7 @@ const postAvatarUrl = (username) => { const acc = String(selectedAccount.value || '').trim() const u = String(username || '').trim() if (!acc || !u) return '' - return `${mediaBase}/api/chat/avatar?account=${encodeURIComponent(acc)}&username=${encodeURIComponent(u)}` + return `${apiBase}/chat/avatar?account=${encodeURIComponent(acc)}&username=${encodeURIComponent(u)}` } const cleanLikeName = (v) => String(v ?? '').replace(/\u00A0/g, ' ').trim() @@ -1460,7 +1458,7 @@ const normalizeMediaUrl = (u) => { try { const host = new URL(raw).hostname.toLowerCase() if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) { - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(raw)}` } } catch {} return raw @@ -1515,8 +1513,10 @@ const getSnsMediaUrl = (post, m, idx, rawUrl) => { if (!raw) return '' const rawLower = raw.toLowerCase() - // If backend already provides a local media endpoint, keep it as-is. - if (rawLower.startsWith('/api/') || rawLower.startsWith('blob:') || rawLower.startsWith('data:')) return raw + // If backend already provides a local media endpoint, rewrite it to the effective API base + // (so web builds with a custom API port still work). + if (rawLower.startsWith('/api/')) return `${apiBase}${raw.slice(4)}` + if (rawLower.startsWith('blob:') || rawLower.startsWith('data:')) return raw // For Moments images/thumbnails, prefer a backend endpoint that can decrypt local cache. if (/^https?:\/\//i.test(raw)) { @@ -1568,7 +1568,7 @@ const getSnsMediaUrl = (post, m, idx, rawUrl) => { // Bump this when changing backend matching logic to avoid stale cached wrong images. parts.set('v', '9') parts.set('url', raw) - return `${mediaBase}/api/sns/media?${parts.toString()}` + return `${apiBase}/sns/media?${parts.toString()}` } } catch {} } @@ -1589,7 +1589,7 @@ const getSnsVideoUrl = (postId, mediaId) => { // 本地缓存视频 const acc = String(selectedAccount.value || '').trim() if (!acc || !postId || !mediaId) return '' - return `${mediaBase}/api/sns/video?account=${encodeURIComponent(acc)}&post_id=${encodeURIComponent(postId)}&media_id=${encodeURIComponent(mediaId)}` + return `${apiBase}/sns/video?account=${encodeURIComponent(acc)}&post_id=${encodeURIComponent(postId)}&media_id=${encodeURIComponent(mediaId)}` } const getSnsRemoteVideoSrc = (post, m) => { @@ -1610,7 +1610,7 @@ const getSnsRemoteVideoSrc = (post, m) => { // When cache is disabled, bust browser caching so backend really downloads+decrypts each time. if (!snsUseCache.value) parts.set('_t', String(Date.now())) parts.set('v', '1') - return `${mediaBase}/api/sns/video_remote?${parts.toString()}` + return `${apiBase}/sns/video_remote?${parts.toString()}` } const localVideoStatus = ref({}) @@ -1726,7 +1726,7 @@ const getLivePhotoVideoSrc = (post, m, idx = 0) => { if (!snsUseCache.value) parts.set('_t', String(Date.now())) // Version bump for frontend cache busting when endpoint changes. parts.set('v', '1') - return `${mediaBase}/api/sns/video_remote?${parts.toString()}` + return `${apiBase}/sns/video_remote?${parts.toString()}` } // 图片预览 + 候选匹配选择 @@ -2114,7 +2114,7 @@ const getProxyExternalUrl = (url) => { // 目前难以计算enc,代理获取封面图(thumbnail) const u = String(url || '').trim() if (!u) return '' - return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(u)}` + return `${apiBase}/chat/media/proxy_image?url=${encodeURIComponent(u)}` } diff --git a/frontend/stores/chatRealtime.js b/frontend/stores/chatRealtime.js index 132256c..2d1a015 100644 --- a/frontend/stores/chatRealtime.js +++ b/frontend/stores/chatRealtime.js @@ -89,8 +89,8 @@ export const useChatRealtimeStore = defineStore('chatRealtime', () => { if (!account) return if (typeof EventSource === 'undefined') return - const base = 'http://localhost:8000' - const url = `${base}/api/chat/realtime/stream?account=${encodeURIComponent(account)}` + const apiBase = useApiBase() + const url = `${apiBase}/chat/realtime/stream?account=${encodeURIComponent(account)}` try { eventSource = new EventSource(url) @@ -223,4 +223,3 @@ export const useChatRealtimeStore = defineStore('chatRealtime', () => { toggle, } }) -