diff --git a/app/routes/plans.py b/app/routes/plans.py index f6f8f52..a3948b5 100644 --- a/app/routes/plans.py +++ b/app/routes/plans.py @@ -763,6 +763,126 @@ def export_md(plan_id): ) +@main_bp.route("/api/plans//docx", methods=["GET"]) +@login_required_json +def export_docx(plan_id): + """导出DOCX - 使用pandoc将markdown转为docx""" + import subprocess, tempfile, os, shutil + from urllib.parse import quote + + 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的逻辑) + report_template = None + try: + from app.models import Template + if template_id: + tmpl = Template.query.get(template_id) + if tmpl and tmpl.type == "report": + report_template = tmpl.content + else: + tmpl = Template.query.filter_by(type="report").order_by(Template.sort_order.asc()).first() + if tmpl: + report_template = tmpl.content + 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 + 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) + + # 转换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") + + with open(md_path, "w", encoding="utf-8") as f: + f.write(md_content) + + try: + subprocess.run( + ["pandoc", md_path, "-o", docx_path], + check=True, capture_output=True + ) + + # 添加水印 + 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}"} + ) + + @main_bp.route("/plans//wechat", methods=["GET"]) @login_required_json def wechat_card(plan_id): diff --git a/app/templates/plan_detail.html b/app/templates/plan_detail.html index a7a25f2..b55ea55 100644 --- a/app/templates/plan_detail.html +++ b/app/templates/plan_detail.html @@ -81,7 +81,10 @@ async function loadPlan() { - + @@ -138,6 +141,12 @@ function downloadMDWithTemplate() { window.open(`/api/plans/${currentPlanId}/md${suffix}`, '_blank'); } +function downloadDOCXWithTemplate() { + const templateId = document.getElementById('reportTemplateSelect')?.value; + const suffix = templateId ? `?template_id=${templateId}` : ''; + window.open(`/api/plans/${currentPlanId}/docx${suffix}`, '_blank'); +} + async function loadTemplates() { try { const resp = await fetch('/templates/templates?type=report');