优化pdf下载

This commit is contained in:
xucheng 2026-01-23 11:39:32 +08:00
parent 66f4914c5a
commit 30e954d89f
4 changed files with 63 additions and 10 deletions

View File

@ -236,7 +236,10 @@ class ExportPDFRequest(BaseModel):
async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(get_db)):
"""Export selected chat sessions to PDF"""
from sqlalchemy import select, desc, asc
from datetime import datetime
from datetime import datetime, timezone, timedelta
# Timezone definition (Shanghai)
shanghai_tz = timezone(timedelta(hours=8))
if not request.session_ids:
raise HTTPException(status_code=400, detail="No sessions selected")
@ -260,7 +263,13 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
messages_html = ""
for log in logs:
timestamp = log.timestamp.strftime('%Y-%m-%d %H:%M:%S') if log.timestamp else ""
# Convert timestamp to Shanghai time
if log.timestamp:
ts_shanghai = log.timestamp.astimezone(shanghai_tz) if log.timestamp.tzinfo else log.timestamp.replace(tzinfo=timezone.utc).astimezone(shanghai_tz)
timestamp = ts_shanghai.strftime('%Y-%m-%d %H:%M:%S')
else:
timestamp = ""
response_html = markdown.markdown(log.response or "", extensions=['tables', 'fenced_code'])
# Add context info (Stock Code / Session) to the header since they are mixed
@ -275,7 +284,6 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
</div>
<div class="message-sub-meta">
<span class="session-id">Session: {session_info}</span>
<span class="model">Model: {log.model}</span>
</div>
<div class="message-content">
{response_html}
@ -284,6 +292,8 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
'''
# Complete HTML
now_shanghai = datetime.now(shanghai_tz)
# Updated font-family stack to include common Linux/Windows CJK fonts
complete_html = f'''
<!DOCTYPE html>
@ -383,7 +393,7 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
</head>
<body>
<h1>AI 研究讨论导出</h1>
<p style="text-align: center; color: #666; margin-bottom: 30px;">导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<p style="text-align: center; color: #666; margin-bottom: 30px;">导出时间: {now_shanghai.strftime('%Y-%m-%d %H:%M:%S')}</p>
{messages_html}
</body>
</html>
@ -395,7 +405,13 @@ async def export_chat_pdf(request: ExportPDFRequest, db: AsyncSession = Depends(
pdf_path = tmp_file.name
HTML(string=complete_html).write_pdf(pdf_path)
filename = f"AI研究讨论汇总_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
# Determine filename prefix based on the most common stock code in logs
# or just the first one found
company_name = "AI研究讨论"
if logs and logs[0].stock_code:
company_name = logs[0].stock_code
filename = f"{company_name}_{now_shanghai.strftime('%Y%m%d')}_讨论记录.pdf"
filename_encoded = quote(filename)
return FileResponse(

View File

@ -343,6 +343,17 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
"""
# Complete PDF HTML
# Timezone conversion for display
from datetime import datetime, timezone, timedelta
shanghai_tz = timezone(timedelta(hours=8))
# Use report creation time for the report date in PDF
report_time = report.created_at.astimezone(shanghai_tz) if report.created_at.tzinfo else report.created_at.replace(tzinfo=timezone.utc).astimezone(shanghai_tz)
# Current time for filename (download date)
now_shanghai = datetime.now(shanghai_tz)
download_date_str = now_shanghai.strftime('%Y%m%d')
complete_html = f"""
<!DOCTYPE html>
<html>
@ -515,8 +526,7 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
<div class="cover">
<h1>{report.company_name}</h1>
<div class="meta">{report.market} {report.symbol}</div>
<div class="meta">分析日期: {report.created_at.strftime('%Y年%m月%d')}</div>
{f'<div class="meta">AI模型: {report.ai_model}</div>' if report.ai_model else ''}
<div class="meta">分析日期: {report_time.strftime('%Y年%m月%d')}</div>
</div>
<div class="section">
@ -538,7 +548,8 @@ async def download_report_pdf(report_id: int, db: AsyncSession = Depends(get_db)
HTML(string=complete_html).write_pdf(pdf_path)
# Return the PDF file
filename = f"{report.company_name}_{report.symbol}_分析报告.pdf"
# Filename format: Company_Symbol_Date_Report.pdf
filename = f"{report.company_name}_{report.symbol}_{download_date_str}_分析报告.pdf"
# Use RFC 5987 encoding for non-ASCII filenames
filename_encoded = quote(filename)

View File

@ -75,7 +75,14 @@ export default function AnalysisPage({ params }: { params: Promise<{ id: string
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${report.company_name}_${report.symbol}_分析报告.pdf`
// Format date as YYYYMMDD to match backend
const date = new Date()
const dateStr = date.getFullYear() +
String(date.getMonth() + 1).padStart(2, '0') +
String(date.getDate()).padStart(2, '0')
a.download = `${report.company_name}_${report.symbol}_${dateStr}_分析报告.pdf`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)

View File

@ -250,11 +250,30 @@ export function HistoryView() {
body: JSON.stringify({ session_ids: Array.from(selectedForExport) })
})
if (!response.ok) throw new Error("导出失败")
// Try to get filename from content-disposition
let filename = `讨论历史_${new Date().toISOString().slice(0, 10)}.pdf`
const disposition = response.headers.get('content-disposition')
if (disposition && disposition.indexOf('attachment') !== -1) {
const filenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:;|$)|filename="([^"]+)"(?:;|$)|filename=([^;]+)(?:;|$)/i;
const matches = filenameRegex.exec(disposition);
if (matches) {
// Try decoding the UTF-8 encoded filename
try {
if (matches[1]) filename = decodeURIComponent(matches[1]);
else if (matches[2]) filename = matches[2];
else if (matches[3]) filename = matches[3];
} catch (e) {
console.error("Error decoding filename:", e);
}
}
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `讨论历史_${new Date().toISOString().slice(0, 10)}.pdf`
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)