diff --git a/app/routes/plans.py b/app/routes/plans.py index a3948b5..2cf2453 100644 --- a/app/routes/plans.py +++ b/app/routes/plans.py @@ -760,25 +760,24 @@ def export_md(plan_id): headers={ 'Content-Disposition': f"attachment; filename*=UTF-8''{filename}" } - ) +) -@main_bp.route("/api/plans//docx", methods=["GET"]) +@main_bp.route("/api/plans//preview", methods=["GET"]) @login_required_json -def export_docx(plan_id): - """导出DOCX - 使用pandoc将markdown转为docx""" - import subprocess, tempfile, os, shutil - from urllib.parse import quote +def preview_report(plan_id): + """预览报告模板渲染结果 - 返回HTML片段""" + import markdown + from app.models import Template plan = PracticePlan.query.get_or_404(plan_id) content = json.loads(plan.content) student_name = plan.student.name if plan.student else "未知学员" - template_id = request.args.get('template_id', type=int) - # 获取报告模板(复用export_md的逻辑) + # 获取选中的报告模板 + template_id = request.args.get('template_id', type=int) report_template = None try: - from app.models import Template if template_id: tmpl = Template.query.get(template_id) if tmpl and tmpl.type == "report": @@ -790,98 +789,59 @@ def export_docx(plan_id): except: pass - if report_template: - rendered = report_template - rendered = rendered.replace("{student_name}", student_name) - rendered = rendered.replace("{practice_time}", content.get('practice_time', 'N/A')) - rendered = rendered.replace("{total_minutes}", str(content.get('total_daily_minutes', 0))) - rendered = rendered.replace("{generated_at}", content.get('generated_at', '')) - from app.models import User - user_id = session.get('user_id') - user_name = '未知' - if user_id: - user = User.query.get(user_id) - if user and user.name: - user_name = user.name - rendered = rendered.replace("{generated_by}", user_name) - if content.get('ai_report'): - rendered = rendered.replace("{ai_report}", content['ai_report']) - else: - rendered = rendered.replace("{ai_report}", "(未生成AI报告)") - problem_tags = "" - for problem in content.get('problems', []): - problem_tags += f"- **{problem.get('name', '')}** ({problem.get('severity', '')})\n" - rendered = rendered.replace("{problem_tags}", problem_tags or "(无)") - # 学员目标 - from app.models import StudentGoal - student_goals_list = StudentGoal.query.filter_by(student_id=plan.student_id).all() if plan.student_id else [] - goals_text_parts = [] - for g in student_goals_list: - if g.status != "已完成": - goals_text_parts.append(f"- **{g.goal.name}**\n {g.goal.content if g.goal else '未提供具体内容'}") - goals_text = "\n".join(goals_text_parts) if goals_text_parts else "(无)" - rendered = rendered.replace("{student_goals}", goals_text) - md_content = rendered + if not report_template: + return "
无可用模板
", 200 + + # 占位符替换(复用 export_pdf 逻辑) + rendered = report_template + rendered = rendered.replace("{student_name}", student_name) + rendered = rendered.replace("{practice_time}", content.get('practice_time', 'N/A')) + rendered = rendered.replace("{total_minutes}", str(content.get('total_daily_minutes', 0))) + rendered = rendered.replace("{generated_at}", content.get('generated_at', '')) + + from app.models import User + user_id = session.get('user_id') + user_name = '未知' + if user_id: + user = User.query.get(user_id) + if user and user.name: + user_name = user.name + rendered = rendered.replace("{generated_by}", user_name) + + if content.get('ai_report'): + rendered = rendered.replace("{ai_report}", content['ai_report']) else: - md_lines = [ - f"# {student_name} - 个性化练习方案\n", - f"**练习时间**: {content.get('practice_time', 'N/A')} (共{content.get('total_daily_minutes', 0)}分钟)\n", - f"**生成时间**: {content.get('generated_at', '')}\n", - "\n---\n", - ] - if content.get('ai_report'): - md_lines.append(content['ai_report']) - md_content = ''.join(md_lines) + rendered = rendered.replace("{ai_report}", "(未生成AI报告)") - # 转换markdown为docx - tmp_dir = tempfile.mkdtemp() - md_path = os.path.join(tmp_dir, f"plan_{plan_id}.md") - docx_path = os.path.join(tmp_dir, f"{student_name}_练习方案.docx") + problem_tags = "" + for problem in content.get('problems', []): + problem_tags += f"- **{problem.get('name', '')}** ({problem.get('severity', '')})\n" + rendered = rendered.replace("{problem_tags}", problem_tags or "(无)") - with open(md_path, "w", encoding="utf-8") as f: - f.write(md_content) + # 学员目标 + from app.models import StudentGoal + student_goals_list = StudentGoal.query.filter_by(student_id=plan.student_id).all() if plan.student_id else [] + goals_text_parts = [] + for g in student_goals_list: + if g.status != "已完成": + goal_content = (g.goal.content if g.goal else '未提供具体内容').replace('\n', '
') + goals_text_parts.append(f"- **{g.goal.name}**
{goal_content}") + goals_text = "
".join(goals_text_parts) if goals_text_parts else "(无)" + rendered = rendered.replace("{student_goals}", goals_text) - try: - subprocess.run( - ["pandoc", md_path, "-o", docx_path], - check=True, capture_output=True - ) + # Markdown 转 HTML + html_content = markdown.markdown(rendered, extensions=['tables', 'fenced_code']) - # 添加水印 - from app.config import load_api_config - api_config = load_api_config(current_app.config) - wm_text = api_config.get("watermark_text", "") - if wm_text: - from docx import Document - doc = Document(docx_path) - section = doc.sections[0] - header = section.header - # 清除现有header内容 - for para in header.paragraphs: - for run in para.runs: - run.text = "" - # 添加水印paragraph - header_para = header.paragraphs[0] if header.paragraphs else header.add_paragraph() - header_para.text = wm_text - header_para.alignment = 1 # CENTER (WD_ALIGN_PARAGRAPH.CENTER = 1) - for run in header_para.runs: - run.font.size = Pt(48) - run.font.color.rgb = RGBColor(180, 180, 180) - run.font.name = "Arial" - doc.save(docx_path) - - with open(docx_path, "rb") as f: - docx_data = f.read() - finally: - shutil.rmtree(tmp_dir, ignore_errors=True) - - filename = quote(f"{student_name}_练习方案.docx") - return Response( - docx_data, - mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document', - headers={'Content-Disposition': f"attachment; filename*=UTF-8''{filename}"} + # 支持 ReportLab 语法,转为 HTML + import re + html_content = re.sub( + r'(.+?)', + r'
\1
', + html_content ) + return html_content, 200, {'Content-Type': 'text/html; charset=utf-8'} + @main_bp.route("/plans//wechat", methods=["GET"]) @login_required_json diff --git a/app/templates/plan_detail.html b/app/templates/plan_detail.html index b55ea55..f053294 100644 --- a/app/templates/plan_detail.html +++ b/app/templates/plan_detail.html @@ -2,6 +2,124 @@ {% block title %}方案详情 - 钢琴练习方案系统{% endblock %} +{% block extra_css %} + +{% endblock %} + {% block content %}

方案详情

@@ -81,10 +199,10 @@ async function loadPlan() { - -
@@ -141,10 +259,45 @@ function downloadMDWithTemplate() { window.open(`/api/plans/${currentPlanId}/md${suffix}`, '_blank'); } -function downloadDOCXWithTemplate() { +async function showPreview() { const templateId = document.getElementById('reportTemplateSelect')?.value; + const planId = currentPlanId; const suffix = templateId ? `?template_id=${templateId}` : ''; - window.open(`/api/plans/${currentPlanId}/docx${suffix}`, '_blank'); + + try { + const resp = await fetch(`/api/plans/${planId}/preview${suffix}`); + if (!resp.ok) { + alert('预览加载失败'); + return; + } + const html = await resp.text(); + document.getElementById('previewContent').innerHTML = html; + + // 加载水印配置 + try { + const cfgResp = await fetch('/api/config'); + if (cfgResp.ok) { + const cfg = await cfgResp.json(); + const wmText = cfg.watermark_text || ''; + const wmEl = document.getElementById('previewWatermark'); + if (wmText) { + wmEl.setAttribute('data-watermark', wmText); + wmEl.classList.add('watermark-active'); + } else { + wmEl.classList.remove('watermark-active'); + } + } + } catch (e) { + console.log('水印配置加载失败', e); + } + + // 显示模态框 + const modal = new bootstrap.Modal(document.getElementById('previewModal')); + modal.show(); + } catch (err) { + console.error('预览错误:', err); + alert('预览加载失败'); + } } async function loadTemplates() { @@ -195,4 +348,20 @@ window.currentStudentId = null; loadPlan(); + + + {% endblock %} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5f7e2a1..2154b2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ Flask-SQLAlchemy==3.1.1 reportlab==4.0.7 Jinja2==3.1.3 requests==2.31.0 -gunicorn==23.0.0 \ No newline at end of file +gunicorn==23.0.0 +markdown>=3.4 \ No newline at end of file