# 设计:个人主页文章详情页导出 Markdown **日期**:2026-04-23 **状态**:已定稿(待你最终确认后进入实现计划) ## 1. 背景与目标 当前个人主页文章详情页支持阅读与评论,但缺少“导出原始 Markdown”能力。 本期目标是在公开文章详情页新增导出能力,满足“访客可直接下载 `.md` 文件”的需求。 ## 2. 本期确认范围 - 导出格式:仅 `Markdown (.md)`。 - 入口位置:个人主页文章详情页(`/@[publicSlug]/posts/[postSlug]`)。 - 权限:所有访客可导出(无需登录)。 - 导出内容:仅正文 `bodyMarkdown`。 - 图片处理:导出时将站内相对图片链接转换为绝对 URL。 ## 3. 非目标 - 不导出 PDF / HTML。 - 不附带 Front Matter 元数据。 - 不打包图片文件到本地,仅处理 Markdown 内图片链接文本。 - 不新增后端导出 API(本期前端本地导出)。 ## 4. 技术方案 采用前端本地导出(Blob + download): 1. 在详情页数据加载完成后显示 `导出 .md` 按钮。 2. 点击按钮时读取当前已加载的 `data.bodyMarkdown`。 3. 仅对 Markdown 图片语法 `![](...)` 中的链接做归一化: - 若为站内相对路径(如 `/static/media/...`),替换为 `window.location.origin + path`。 - 其他链接类型保持不变。 4. 生成 `Blob(['...'], { type: 'text/markdown;charset=utf-8' })`。 5. 通过临时 `` 触发下载,文件名按规则生成。 该方案不新增接口,改动范围小,符合“仅导出正文、访客可用、快速交付”的目标。 ## 5. URL 归一化规则 ### 5.1 处理对象 - 仅处理 Markdown 图片语法:`![](...)`。 - 普通链接语法 `[](...)` 不处理。 ### 5.2 保持不变 - `http://...`、`https://...` - `//...`(协议相对) - `data:image...` ### 5.3 转换条件 - 以 `/` 开头的站内相对路径(重点覆盖 `/static/media/...`)。 ### 5.4 基地址来源 - 使用 `window.location.origin` 作为导出时站点基地址。 ### 5.5 示例 - 输入:`![](/static/media/u1/a.webp)` - 当前访问域名:`https://example.com` - 输出:`![](https://example.com/static/media/u1/a.webp)` ## 6. 交互与错误处理 - 按钮位置:详情页顶部操作区,与“返回主页 / 编辑”同级。 - 显示与可用性: - `pending` 状态禁用按钮。 - `error && !data` 时不显示按钮。 - `data` 就绪后访客可点击导出。 - 成功反馈:导出完成后给轻量 Toast(如“已导出 Markdown”)。 - 失败反馈:捕获异常并提示“导出失败,请稍后重试”,不影响页面其他功能。 - 空正文:允许导出空 `.md` 文件。 ## 7. 文件命名规则 - 优先:`${postSlug}.md` - 回退:`post-${id}.md` 确保命名稳定、可预测,便于用户归档。 ## 8. 验收标准 ### 8.1 功能验收 - 公开文章详情页可见 `导出 .md` 按钮,未登录访客可使用。 - 点击后下载文件名符合规则。 - 导出正文与 `bodyMarkdown` 一致(仅图片链接归一化除外)。 ### 8.2 图片链接验收 - `![](/static/media/xxx.png)` 转换为当前域名下绝对 URL。 - 已是绝对地址、协议相对地址、data URI 的图片链接保持不变。 - 普通链接 `[](...)` 不被误改。 ### 8.3 状态与异常 - 加载中按钮禁用;加载失败场景不显示导出按钮。 - 成功/失败提示可见且文案清晰。 - 异常不导致页面崩溃。 ### 8.4 回归检查 - 原有“返回主页”“编辑(作者可见)”“评论”功能不受影响。 - 移动端与桌面端按钮布局保持可用。 ## 9. 实现建议(供下一步计划使用) - 修改页面:`app/pages/@[publicSlug]/posts/[postSlug].vue` - 可抽离工具函数(可选):`app/utils/markdown-export.ts` - `normalizeMarkdownImageUrls(markdown: string, origin: string): string` - `downloadMarkdownFile(filename: string, content: string): void` 后续进入 implementation plan 时,可根据当前代码风格决定“内联实现”或“工具函数抽离”。