e4f850672c
Co-Authored-By: Claude <noreply@anthropic.com>
116 lines
6.9 KiB
Markdown
116 lines
6.9 KiB
Markdown
# 导出会话聊天为 HTML(③,ChatTimeline 接线 + 文件名修复)设计
|
||
|
||
> 日期:2026-08-08|分支:`feat/yunke-data-mirror`|路线图 ③
|
||
> 前置(已就绪):后端 `GET /api/export/chat-html`(`app/api/routers/export.py`)已实现、已注册、有测试——按 (channel, owner_account_id, talker_id|room_id, start?, end?) 拉消息 → 渲染自包含 HTML 气泡页 → attachment 下载,文件名 `chat-{channel}-{owner}-{peer}.html`。
|
||
> 数据/交互:全本地。云客「导出聊天记录」2026-08-08 真机核实(异步任务→下载中心);用户定:**按我们内部逻辑做,不复刻异步下载中心**。
|
||
|
||
## 1. 目标与范围
|
||
|
||
**现状(plan 核实,2026-08-08)**:
|
||
- 后端 `/api/export/chat-html` 已就绪(export.py,不改)。
|
||
- **CustomerList、GroupList 操作列「导出HTML聊天记录」按钮已存在**(wechat tab;用 `downloadBlob('/export/chat-html', params)`,CustomerList 还带时间范围选择 modal)。**本 plan 不动这两处。**
|
||
- **ChatTimeline(聊天窗)缺「导出」按钮** —— 唯一缺失的入口。
|
||
- `downloadBlob`(`api/client.ts`)有个 **bug**:因响应拦截器 `(response)=>response.data` 丢头,文件名硬编码 `'chat-export.html'`,丢失后端 `chat-{...}.html` 命名(影响已存的 2 个按钮 + 即将加的 ChatTimeline 按钮)。
|
||
|
||
**目标**:补 ChatTimeline「导出」入口 + 修 `downloadBlob` 文件名丢失。
|
||
|
||
**在本 plan(纯前端)**:
|
||
1. **ChatTimeline 头部加「导出」按钮**(`v-if="hasActive"`)→ `downloadBlob('/export/chat-html', {channel, owner_account_id, talker_id/room_id, start: winStart, end: winEnd})`,即导出当前会话在当前 dateRange 内的聊天 HTML。沿用现有 `downloadBlob`(与 CustomerList/GroupList 一致,不引入 window.open 第二套下载路径)。
|
||
2. **修 `downloadBlob` 文件名**:改用裸 `axios.get`(不经解包拦截器)拿全响应 → 从 `Content-Disposition` 解析文件名(`chat-{channel}-{owner}-{peer}.html`),fallback `'chat-export.html'`。惠及 3 处按钮。
|
||
|
||
**不在本 plan**:
|
||
- 云客异步任务/下载中心(不需要)。
|
||
- CustomerList/GroupList 导出改造(已实现,不动;WA tab 无导出 = 与云客一致,保留)。
|
||
- HTML 排版改版(沿用 export.py)。
|
||
- ChatTimeline 的导出时间范围 modal:聊天窗已有 dateRange(step 5),直接用,不加 modal。
|
||
|
||
## 2. 后端契约(已就绪,引用,不改)
|
||
|
||
`GET /api/export/chat-html`:query `channel`/`owner_account_id`(必)/`talker_id?`/`room_id?`/`start?`/`end?`(ms) → `text/html` attachment,`Content-Disposition: attachment; filename="chat-{channel}-{owner}-{peer}.html"`。
|
||
|
||
## 3. 前端
|
||
|
||
### 3.1 `downloadBlob` 文件名修复(`yunkes/src/api/client.ts`)
|
||
当前用 `http.get(url,{params,responseType:'blob'})` 但拦截器把响应解包成 body(丢头)。改为裸 `axios`(不经拦截器):
|
||
```typescript
|
||
import axios from 'axios' // 已 import
|
||
|
||
/** 以 blob 下载(HTML 导出等附件)。从 Content-Disposition 取文件名(绕开解包拦截器)。 */
|
||
export async function downloadBlob(
|
||
url: string,
|
||
params?: Record<string, unknown>,
|
||
): Promise<string> {
|
||
// 裸 axios(baseURL '/api')拿全响应(含 headers),不经 http 的 (r)=>r.data 拦截器
|
||
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
|
||
document.body.appendChild(a)
|
||
try {
|
||
a.click()
|
||
} finally {
|
||
document.body.removeChild(a)
|
||
URL.revokeObjectURL(objUrl)
|
||
}
|
||
return filename
|
||
}
|
||
```
|
||
> `url` 入参仍是相对 `/export/chat-html`(与 CustomerList 现有调用一致),函数内补 `/api` 前缀。
|
||
|
||
### 3.2 ChatTimeline 头部「导出」按钮(`yunkes/src/views/ChatTimeline.vue`)
|
||
- import `{ downloadBlob } from '@/api/client'`(或经 `@/api` 若已 re-export;plan 核实)。
|
||
- `.yk-chat__filters` 区(step 5,含日期选择器/搜索)加:
|
||
```vue
|
||
<el-button v-if="hasActive" size="small" type="primary" plain @click="exportCurrent">导出</el-button>
|
||
```
|
||
```typescript
|
||
async function exportCurrent(): Promise<void> {
|
||
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}`)
|
||
}
|
||
}
|
||
```
|
||
> `ElMessage` 已 import。`channelTyped`/`isGroup`/`props`/`winStart`/`winEnd`/`hasActive` 均存在(step 5/Task4)。
|
||
|
||
## 4. 关键决策(已与用户确认 + spec review 纠正 2026-08-08)
|
||
|
||
1. **③ = 仅 ChatTimeline 加导出按钮 + 修 downloadBlob 文件名**(CustomerList/GroupList 已实现,不动)。
|
||
2. **沿用 `downloadBlob`**(与现存 2 处一致),**不引入** window.open/exportChatUrl 第二套路径(spec review 指出)。
|
||
3. **修文件名 bug**(惠及 3 处):裸 axios 保头 + 解析 Content-Disposition。
|
||
4. **同步直接下载**(无云客异步下载中心);HTML 渲染沿用 export.py。
|
||
5. ChatTimeline 导出用现有 dateRange(不加 modal);CustomerList 的时间 modal 已存,不动。
|
||
|
||
## 5. 文件清单
|
||
|
||
- 改 `yunkes/src/api/client.ts`(`downloadBlob` 文件名修复)。
|
||
- 改 `yunkes/src/views/ChatTimeline.vue`(头部「导出」按钮 + `exportCurrent`)。
|
||
- **无后端改动**;CustomerList/GroupList/export.py 不动。
|
||
|
||
## 6. 验收
|
||
|
||
- `vue-tsc -b` exit 0。
|
||
- **浏览器实测**(本地 smoke.db,无云客):
|
||
- ChatTimeline:进一个会话 → 「导出」→ 下载文件名 `chat-wechat-{owner}-{talker}.html`(**文件名正确**,非 chat-export.html);打开含该 dateRange 内消息气泡(员工右/客户左、时间、文本);改 dateRange 再导出 → 内容随窗变。
|
||
- (回归)CustomerList/GroupList「导出HTML聊天记录」→ 文件名也变正确(`chat-...html`),行为不变。
|
||
- network:仅 `/api/export/chat-html`(无云客)。
|
||
|
||
## 7. 风险
|
||
|
||
- **裸 axios 不带拦截器的错误处理**:`downloadBlob` 失败时裸 axios 抛原始 axios 错(无统一 message 包装);`exportCurrent` 用 try/catch + ElMessage 兜底(CustomerList 现状亦如此)。
|
||
- **Content-Disposition 解析**:正则 `filename="..."`;后端用双引号包裹(export.py 确认);fallback 固定名。
|
||
- **WA/群导出**:ChatTimeline 群会话(isGroup)传 room_id;WA 通道同理(channel=whatsapp)。WA tab 列表导出不在本 plan(与云客一致,无)。
|