Commit 3687f62d authored by Data Governance Dev's avatar Data Governance Dev

fix(web): 报告下载链路修复(Markdown 按钮 + 字节流下载 + 路由路径)

三处问题一起修:

1. Markdown 按钮点了没反应
   - 按钮 @click 传 'md',但 jobResult.reports 里的 key 是 'markdown'
   - downloadReport 拿不到 url,静默 return
   - index.html:188 downloadReport('md') → downloadReport('markdown')

2. 改为字节流下载
   - 原方案 window.open('/api/reports/...'):浏览器把 .md 直接显示在页面上
   - 改 fetch + blob + <a download>:触发浏览器「另存为」对话框,体验正确
   - 路径正则兼容正反斜杠(Windows/Linux)
   - 三段(db/run_id/filename)都 encodeURIComponent
   - 错误用 ElementPlus.ElMessage.error 提示,不再静默退出

3. API 路由路径少一段导致 404
   - routes.py 在 web/api/,少算一层 base 就跨过了 web/ 目录
   - 原 base = <项目根>/outputs/...,实际文件在 <项目根>/web/outputs/...
   - .parent.parent.parent → .parent.parent,与 job_manager.py 保持一致
parent 3f71fa27
...@@ -273,7 +273,7 @@ async def download_report(database: str, run_id: str, filename: str): ...@@ -273,7 +273,7 @@ async def download_report(database: str, run_id: str, filename: str):
# 防止路径穿越 # 防止路径穿越
if ".." in run_id or ".." in filename or "/" in filename or "\\" in filename: if ".." in run_id or ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="非法文件名") raise HTTPException(status_code=400, detail="非法文件名")
base = Path(__file__).resolve().parent.parent.parent / "outputs" / database / run_id / "reports" base = Path(__file__).resolve().parent.parent / "outputs" / database / run_id / "reports"
target = base / filename target = base / filename
if not target.exists(): if not target.exists():
raise HTTPException(status_code=404, detail="报告不存在") raise HTTPException(status_code=404, detail="报告不存在")
......
...@@ -184,7 +184,7 @@ ...@@ -184,7 +184,7 @@
<el-icon><DataLine /></el-icon> <el-icon><DataLine /></el-icon>
<span class="card-title">③ 分析结果</span> <span class="card-title">③ 分析结果</span>
<div> <div>
<el-button v-if="jobResult.reports.markdown" size="small" @click="downloadReport('md')"> <el-button v-if="jobResult.reports.markdown" size="small" @click="downloadReport('markdown')">
<el-icon><Download /></el-icon>Markdown <el-icon><Download /></el-icon>Markdown
</el-button> </el-button>
<el-button v-if="jobResult.reports.docx" size="small" type="primary" @click="downloadReport('docx')"> <el-button v-if="jobResult.reports.docx" size="small" type="primary" @click="downloadReport('docx')">
...@@ -963,18 +963,37 @@ ...@@ -963,18 +963,37 @@
} }
// ── 下载报告 ── // ── 下载报告 ──
function downloadReport(fmt) { async function downloadReport(fmt) {
if (!jobResult.value || !jobResult.value.reports) return; if (!jobResult.value || !jobResult.value.reports) return;
const url = jobResult.value.reports[fmt]; const path = jobResult.value.reports[fmt];
if (!url) return; if (!path) return;
// 后端返回的 url 形如 .../outputs/<db>/<run_id>/reports/<filename> // 后端 reports[fmt] 返回的是绝对路径,形如:
// 后端路由只匹配 3 段(database/run_id/filename),前端需把 reports/ 段剥掉 // C:\...\web\outputs\<database>\<run_id>\reports\<filename>
const m = url.match(/\/outputs\/([^/]+)\/([^/]+)\/reports\/(.+)$/); // 正则兼容正反斜杠(跨平台),从路径尾部倒推
const m = path.match(/[\\/]outputs[\\/]([^\\/]+)[\\/]([^\\/]+)[\\/]reports[\\/](.+)$/);
if (!m) { if (!m) {
console.warn('无法解析报告 URL:', url); console.warn('无法解析报告路径:', path);
ElementPlus.ElMessage.error(`报告路径无法解析: ${path}`);
return; return;
} }
window.open(`/api/reports/${m[1]}/${m[2]}/${m[3]}`, '_blank'); const [, database, runId, filename] = m;
const apiUrl = `/api/reports/${encodeURIComponent(database)}/${encodeURIComponent(runId)}/${encodeURIComponent(filename)}`;
try {
const resp = await fetch(apiUrl);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const blob = await resp.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
} catch (e) {
console.error('下载失败:', e);
ElementPlus.ElMessage.error(`下载失败: ${e.message || e}`);
}
} }
// ── 按表汇总统计(用于 alert 标题) ── // ── 按表汇总统计(用于 alert 标题) ──
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment