docs(plan): 导出会话HTML(③)实现计划(ChatTimeline 按钮 + downloadBlob 文件名修复)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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'`。
|
||||
Reference in New Issue
Block a user