新增:
- 导出当前解析结果
- 导出当前学生错题本
- 导出学习建议
- 新开页面生成练习单
- 浏览器里直接
打印 / 存成 PDF
后端新增导出接口
1)修改 backend/app/main.py
先补充 import:
javascript
from fastapi.responses import HTMLResponse
2)新增一个导出页面接口
把下面接口加到 main.py 里:
xml
@app.get("/api/export/report", response_class=HTMLResponse)
def export_report_html(
student_id: int = Query(1),
db: Session = Depends(get_db)
):
student = db.query(Student).filter(Student.id == student_id).first()
student_name = student.name if student else f"学生{student_id}"
rows = (
db.query(QuestionHistory)
.filter(QuestionHistory.student_id == student_id)
.order_by(QuestionHistory.id.desc())
.all()
)
report = build_learning_report(db, student_id)
suggestion = build_study_suggestion(db, student_id)
wrong_rows = [row for row in rows if row.is_wrong][:10]
wrong_html = ""
for row in wrong_rows:
try:
knowledge_points = json.loads(row.knowledge_points or "[]")
except Exception:
knowledge_points = []
try:
steps = json.loads(row.steps or "[]")
except Exception:
steps = []
wrong_html += f"""
<div class="card">
<h3>错题 #{row.id}</h3>
<p><strong>题目:</strong>{row.question}</p>
<p><strong>答案:</strong>{row.answer}</p>
<p><strong>知识点:</strong>{'、'.join(knowledge_points) if knowledge_points else '无'}</p>
<div>
<strong>步骤解析:</strong>
<ol>
{''.join([f'<li>{step}</li>' for step in steps])}
</ol>
</div>
</div>
"""
top_kp_html = "".join([
f"<li>{item['name']}({item['count']}次)</li>"
for item in report["top_knowledge_points"]
])
weak_html = "".join([
f"""
<div class="card">
<h3>{item['name']}</h3>
<p>错误 {item['wrong_count']} 次 / 共出现 {item['total_count']} 次 / 错误率 {item['wrong_rate']}%</p>
<p>{item['suggestion']}</p>
</div>
"""
for item in suggestion["weak_knowledge_points"]
])
html = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>{student_name} - 学习练习单</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
margin: 0;
padding: 24px;
color: #222;
background: #f7f8fa;
}}
.container {{
max-width: 960px;
margin: 0 auto;
background: #fff;
padding: 32px;
border-radius: 16px;
}}
h1, h2, h3 {{
margin-top: 0;
}}
.summary {{
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 24px;
}}
.summary-item {{
background: #f5f7fa;
border-radius: 12px;
padding: 16px;
text-align: center;
}}
.summary-label {{
color: #666;
font-size: 14px;
margin-bottom: 8px;
}}
.summary-value {{
font-size: 28px;
font-weight: bold;
color: #18a058;
}}
.card {{
background: #fafafa;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}}
.section {{
margin-top: 32px;
}}
.print-bar {{
margin-bottom: 24px;
}}
.print-btn {{
padding: 10px 16px;
border: none;
background: #18a058;
color: #fff;
border-radius: 8px;
cursor: pointer;
}}
@media print {{
body {{
background: #fff;
padding: 0;
}}
.container {{
max-width: none;
border-radius: 0;
padding: 0;
}}
.print-bar {{
display: none;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="print-bar">
<button class="print-btn" onclick="window.print()">打印 / 保存为 PDF</button>
</div>
<h1>{student_name} - 学习练习单</h1>
<div class="summary">
<div class="summary-item">
<div class="summary-label">总题数</div>
<div class="summary-value">{report['total_count']}</div>
</div>
<div class="summary-item">
<div class="summary-label">错题数</div>
<div class="summary-value">{report['wrong_count']}</div>
</div>
<div class="summary-item">
<div class="summary-label">正确数</div>
<div class="summary-value">{report['correct_count']}</div>
</div>
<div class="summary-item">
<div class="summary-label">错题率</div>
<div class="summary-value">{report['wrong_rate']}%</div>
</div>
</div>
<div class="section">
<h2>整体学习建议</h2>
<div class="card">{suggestion['overall_suggestion']}</div>
</div>
<div class="section">
<h2>高频知识点</h2>
<div class="card">
<ul>{top_kp_html or '<li>暂无</li>'}</ul>
</div>
</div>
<div class="section">
<h2>薄弱知识点分析</h2>
{weak_html or '<div class="card">暂无薄弱知识点</div>'}
</div>
<div class="section">
<h2>错题本(最近10题)</h2>
{wrong_html or '<div class="card">暂无错题</div>'}
</div>
</div>
</body>
</html>
"""
return html
前端新增导出按钮
修改 frontend/src/api/math.ts
新增一个方法:
javascript
export function getExportReportUrl(student_id: number) {
return `http://127.0.0.1:8000/api/export/report?student_id=${student_id}`
}
修改 frontend/src/App.vue
1)补充 import
把 getExportReportUrl 加进去:
python
import {
solveMathQuestion,
solveMathImage,
getHistoryList,
getWrongQuestionList,
markWrongQuestion,
generatePracticeByKnowledge,
regenerateQuestion,
getLearningReport,
getStudySuggestion,
getStudentList,
createStudent,
getExportReportUrl,
type SolveResponse,
type HistoryItem,
type PracticeQuestionItem,
type LearningReportResponse,
type StudySuggestionResponse,
type StudentItem,
} from './api/math'
2)新增导出方法
在 script setup 里新增:
javascript
const handleExportReport = () => {
const url = getExportReportUrl(currentStudentId.value)
window.open(url, '_blank')
}
页面里加导出入口
修改 frontend/src/App.vue
在学生切换区域 student-bar 里,最后加一个按钮:
arduino
<button class="wrong-btn" @click="handleExportReport">
导出练习单
</button>
重启看效果
重启后端
lua
uvicorn app.main:app --reload --port 8000
重启前端
arduino
npm run dev



不同学生 也没问题

