docs(plan): 导出会话HTML(③)实现计划(ChatTimeline 按钮 + downloadBlob 文件名修复)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-08 17:04:53 +08:00
parent e4f850672c
commit e487556253
@@ -0,0 +1,127 @@
# 导出会话聊天为 HTML(③)Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development or superpowers:executing-plans. Steps use checkbox (`- [ ]`).
**Goal:** ChatTimeline 聊天窗加「导出」按钮(触发已有 `/api/export/chat-html`)+ 修 `downloadBlob` 文件名丢失 bug(惠及 3 处导出按钮)。
**Architecture:** 纯前端 2 文件。`client.ts` 的 `downloadBlob` 改用裸 `axios`(不经解包拦截器)保 `Content-Disposition` → 正确文件名;`ChatTimeline.vue` 头部加「导出」`el-button` → `downloadBlob('/export/chat-html', {channel,owner,talker|room,start,end})` 用当前 dateRange。后端 export.py 不动;CustomerList/GroupList 已有导出按钮不动。
**Tech Stack:** Vue3+TS+Element Plus(`yunkes/`);门禁 `vue-tsc -b`。无后端。
**Spec:** `docs/superpowers/specs/2026-08-08-yunke-export-chat-html-design.md`(已过审 + 已纠正)。
---
## 关键事实(plan 核实)
- 后端 `GET /api/export/chat-html`(export.py)已就绪:参数 channel/owner_account_id/talker_id?/room_id?/start?/end?,返回 `text/html`,`Content-Disposition: attachment; filename="chat-{channel}-{owner}-{peer}.html"`。**不改。**
- `client.ts` 现状:`http = axios.create({baseURL:'/api'})` + 响应拦截器 `(r)=>r.data`(解包 body,丢头)。`downloadBlob`(46-66)用 `http.get(url,{params,responseType:'blob'})` → `res` 已是 Blob(头丢)→ 文件名硬编码 `'chat-export.html'`。
- CustomerList(:201 `downloadBlob('/export/chat-html', params)` + 时间 modal)、GroupList(:166)已有导出按钮(wechat tab)。**不动。**
- ChatTimeline(step 5/Task4 后):`defineProps<{channel,ownerId,talkerId?,roomId?}>`、`channelTyped`(44)、`isGroup`(45)、`winStart/winEnd`(103-104)、`hasActive`(56)、`ElMessage` 已 import、`.yk-chat__filters` 区已存在(含日期选择器/搜索)。**缺「导出」按钮。**
- `axios` 已在 client.ts import。
## 文件清单
- 改 `yunkes/src/api/client.ts`(`downloadBlob` 文件名修复)。
- 改 `yunkes/src/views/ChatTimeline.vue`(头部「导出」按钮 + `exportCurrent`)。
- 无后端、无其它文件。
---
## Task 1: downloadBlob 文件名修复 + ChatTimeline 导出按钮
**Files:** `yunkes/src/api/client.ts`、`yunkes/src/views/ChatTimeline.vue`。
- [ ] **Step 1: `client.ts` — `downloadBlob` 改裸 axios 保头**
把现有 `downloadBlob`(46-66)替换为:
```typescript
/** 以 blob 下载(HTML 导出等附件)。从 Content-Disposition 取文件名(绕开 http 的 (r)=>r.data 解包拦截器)。 */
export async function downloadBlob(
url: string,
params?: Record<string, unknown>,
): Promise<string> {
// 裸 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
document.body.appendChild(a)
try {
a.click()
} finally {
document.body.removeChild(a)
URL.revokeObjectURL(objUrl)
}
return filename
}
```
> `url` 入参仍是相对 `/export/chat-html`(与 CustomerList/GroupList 现有调用一致);函数内补 `/api`。错误处理:裸 axios 抛原始错,调用方 try/catch 兜底(CustomerList 现状亦如此)。
- [ ] **Step 2: `ChatTimeline.vue` — import + exportCurrent + 按钮**
import(`ElMessage` 已在;加 `downloadBlob`):
```typescript
import { downloadBlob } from '@/api/client'
```
> 若 `@/api` 已 re-export `downloadBlob` 则从 `@/api` 导;否则从 `@/api/client`。plan 核实(grep `export.*downloadBlob` in api/index.ts)。
加 handler(放 `onSelectSession`/`loadInitial` 附近):
```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}`)
}
}
```
模板 `.yk-chat__filters` 区加(在日期选择器/搜索旁):
```vue
<el-button v-if="hasActive" size="small" type="primary" plain @click="exportCurrent">导出</el-button>
```
- [ ] **Step 3: vue-tsc**
```bash
cd /d/refactor/yunke/yunkes && npx vue-tsc -b
```
Expected: exit 0。
- [ ] **Step 4: 浏览器实测**
启动后端(smoke.db)+前端。`/chat/wechat/{owner}/{talker}`(有消息的会话)→「导出」→ 下载文件名 `chat-wechat-{owner}-{talker}.html`(非 chat-export.html);打开含该 dateRange 内消息气泡。改 dateRange 再导出 → 内容随窗变。
(回归)`/customers` → 操作列「导出HTML聊天记录」→ 文件名也正确(`chat-...html`)。
- [ ] **Step 5: Commit**
```bash
git add yunkes/src/api/client.ts yunkes/src/views/ChatTimeline.vue
git commit -m "feat(chat): ChatTimeline 导出按钮 + 修复 downloadBlob 文件名丢失(Content-Disposition)"
```
End with blank line + `Co-Authored-By: Claude <noreply@anthropic.com>`。
---
## 验收(spec §6)
- [ ] `vue-tsc -b` exit 0。
- [ ] 浏览器(本地 smoke.db):ChatTimeline「导出」→ 文件名正确 + 内容随 dateRange;CustomerList/GroupList 导出文件名也修复(回归);network 仅 `/api/export/chat-html`。
## 风险
- **downloadBlob import 路径**:`@/api` 是否 re-export `downloadBlob`?plan grep 核实;否则从 `@/api/client`。
- **裸 axios 错误**:无拦截器包装,调用方 try/catch + ElMessage(已加)。
- **filename 正则**:后端双引号包裹(export.py 确认);fallback `'chat-export.html'`。