From bb328ac59aaa7b5678abcffb67c984f6442e31c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Sat, 8 Aug 2026 17:07:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20ChatTimeline=20=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=8C=89=E9=92=AE=20+=20=E4=BF=AE=E5=A4=8D=20downloadBlob=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=8D=E4=B8=A2=E5=A4=B1(Content-Dispositi?= =?UTF-8?q?on)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- yunkes/src/api/client.ts | 13 +++++++------ yunkes/src/views/ChatTimeline.vue | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/yunkes/src/api/client.ts b/yunkes/src/api/client.ts index e319fe2..1acc444 100644 --- a/yunkes/src/api/client.ts +++ b/yunkes/src/api/client.ts @@ -42,16 +42,17 @@ export function getJson(url: string, params?: Record): Promi return http.get(url, { params }) as unknown as Promise } -/** 以 blob 下载(用于 HTML 导出等二进制/文本附件下载)。返回固定文件名(响应拦截器丢失 content-disposition 头)。 */ +/** 以 blob 下载(HTML 导出等附件)。从 Content-Disposition 取文件名(绕开 http 的 (r)=>r.data 解包拦截器)。 */ export async function downloadBlob( url: string, params?: Record, ): Promise { - const res = await http.get(url, { params, responseType: 'blob' }) - const blob = res as unknown as Blob - // 从响应头取文件名(拦截器只解包 body,需用 raw 拦截;此处 res 已是 blob,header 丢失 → 用固定名) - const filename = 'chat-export.html' - const objUrl = URL.createObjectURL(blob) + // 裸 axios(补 /api 前缀)拿全响应(含 headers),不经 http 拦截器 + const res = await axios.get(`/api${url}`, { params, responseType: 'blob' }) + const cd = (res.headers['content-disposition'] as string) || '' + const m = /filename="?([^";]+)"?/i.exec(cd) + const filename = m ? m[1] : 'chat-export.html' + const objUrl = URL.createObjectURL(res.data as Blob) const a = document.createElement('a') a.href = objUrl a.download = filename diff --git a/yunkes/src/views/ChatTimeline.vue b/yunkes/src/views/ChatTimeline.vue index 94b1706..26d7acf 100644 --- a/yunkes/src/views/ChatTimeline.vue +++ b/yunkes/src/views/ChatTimeline.vue @@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { ArrowLeft } from '@element-plus/icons-vue' import { getMessages } from '@/api' +import { downloadBlob } from '@/api/client' import type { Channel, Message } from '@/api/types' import MessageBubble from '@/components/MessageBubble.vue' import ChatSidebar from '@/components/ChatSidebar.vue' @@ -228,6 +229,23 @@ function onSelectSession(s: { } } +/** 导出当前会话/时段聊天记录为自包含 HTML(后端渲染气泡 + 附件内联)。 */ +async function exportCurrent(): Promise { + if (!hasActive.value) return + try { + await downloadBlob('/export/chat-html', { + channel: channelTyped.value, + owner_account_id: props.ownerId, + talker_id: isGroup.value ? undefined : props.talkerId, + room_id: isGroup.value ? props.roomId : undefined, + start: winStart.value, + end: winEnd.value, + }) + } catch (e) { + ElMessage.error(`导出失败:${(e as Error).message}`) + } +} + // All-fields drawer state for the message detail view. const drawerOpen = ref(false) const drawerMessage = ref(null) @@ -297,6 +315,7 @@ onMounted(loadInitial) clearable style="width: 200px" /> + 导出