如何使用FastAPI同时返回PDF文件与Jinja2 TemplateResponse?
问题解答
HTTP一次请求只能返回一个响应,所以没法直接同时返回FileResponse和Jinja2 TemplateResponse。不过可以用两种方式实现类似需求,既给用户展示页面,又提供PDF下载:
先给你的代码加中文注释
def generate_report(request: Request, start_date: date = Form(...), end_date: Optional[date] = Form(None)): # 将前端传来的date对象转为datetime对象 start_date = datetime(start_date.year, start_date.month, start_date.day) end_date = datetime(end_date.year, end_date.month, end_date.day) # 按日期筛选考勤数据 attendance = filter_by_date(zk, user_list, start_date, end_date) # 把考勤数据转成字典格式方便处理 users_history = attendance_to_dict(attendance) # 统计用户工作日天数 worked = count_days(users_history, 0) # 生成PDF文档 pdf = create_pdf(users_history, worked, user_list, start_date, end_date) # 临时保存PDF到本地 pdf_temp = "attendance.pdf" pdf.output(pdf_temp) # 用户下载时显示的文件名 name = "report.pdf" # 返回PDF文件让用户下载 return FileResponse(pdf_temp, media_type="application/pdf", filename=name)
方案1:返回页面后自动触发PDF下载
先渲染一个结果页面,然后用页面里的JavaScript自动发起请求下载PDF,用户既看到页面,又能自动拿到文件。
代码修改示例
先拆分出专门返回PDF的接口,原接口返回模板页面:
from fastapi.templating import Jinja2Templates # 初始化模板引擎,指定模板文件所在目录 templates = Jinja2Templates(directory="templates") # 专门用来返回PDF的接口 def get_report_pdf(start_date: date, end_date: Optional[date] = None): start_date = datetime(start_date.year, start_date.month, start_date.day) end_date = datetime(end_date.year, end_date.month, end_date.day) if end_date else datetime.now() attendance = filter_by_date(zk, user_list, start_date, end_date) users_history = attendance_to_dict(attendance) worked = count_days(users_history, 0) pdf = create_pdf(users_history, worked, user_list, start_date, end_date) pdf_temp = "attendance.pdf" pdf.output(pdf_temp) return FileResponse(pdf_temp, media_type="application/pdf", filename="report.pdf") # 原接口返回模板页面,页面自动触发下载 def generate_report(request: Request, start_date: date = Form(...), end_date: Optional[date] = Form(None)): # 把日期参数传给模板,方便JS使用 context = { "request": request, "start_date": start_date.isoformat(), "end_date": end_date.isoformat() if end_date else "" } return templates.TemplateResponse("report_result.html", context)
然后在templates/report_result.html里加JS逻辑:
<!DOCTYPE html> <html> <head> <title>报告生成完成</title> </head> <body> <h1>报告已生成,正在自动下载...</h1> <p>如果下载没自动开始,<a id="manual-download">点这里手动下载</a></p> <script> // 从模板获取日期参数 const startDate = "{{ start_date }}"; const endDate = "{{ end_date }}"; // 构建PDF请求的URL let downloadUrl = `/get-report-pdf?start_date=${startDate}`; if (endDate) { downloadUrl += `&end_date=${endDate}`; } // 创建隐藏的a标签触发下载 const downloadLink = document.createElement('a'); downloadLink.href = downloadUrl; downloadLink.style.display = 'none'; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); // 给手动下载链接赋值 document.getElementById('manual-download').href = downloadUrl; </script> </body> </html>
方案2:返回页面,让用户手动点击下载按钮
直接渲染一个包含下载按钮的页面,用户看到页面后,自己点击按钮下载PDF,逻辑更简单。
代码修改示例
def generate_report(request: Request, start_date: date = Form(...), end_date: Optional[date] = Form(None)): # 先生成PDF文件(和你原来的逻辑一致) start_date = datetime(start_date.year, start_date.month, start_date.day) end_date = datetime(end_date.year, end_date.month, end_date.day) attendance = filter_by_date(zk, user_list, start_date, end_date) users_history = attendance_to_dict(attendance) worked = count_days(users_history, 0) pdf = create_pdf(users_history, worked, user_list, start_date, end_date) pdf_temp = "attendance.pdf" pdf.output(pdf_temp) # 返回带下载按钮的模板页面 context = { "request": request, "pdf_name": "report.pdf" } return templates.TemplateResponse("report_download.html", context)
对应的templates/report_download.html:
<!DOCTYPE html> <html> <head> <title>报告生成完成</title> </head> <body> <h1>报告已准备好</h1> <a href="/download-pdf" download="{{ pdf_name }}">点击下载PDF报告</a> </body> </html>
最后还要加一个专门处理下载的接口:
def download_pdf(): return FileResponse("attendance.pdf", media_type="application/pdf", filename="report.pdf")
小提醒
生成的临时PDF文件记得定期清理,比如用Python的tempfile模块创建临时文件,避免占满磁盘空间。
内容的提问来源于stack exchange,提问作者Ukhu
相关产品推荐
相关产品推荐

