diff --git a/app/routes/plans.py b/app/routes/plans.py index 2cf2453..a961b1f 100644 --- a/app/routes/plans.py +++ b/app/routes/plans.py @@ -832,6 +832,29 @@ def preview_report(plan_id): # Markdown 转 HTML html_content = markdown.markdown(rendered, extensions=['tables', 'fenced_code']) + # URL 转二维码图片(预览用) + import re + import qrcode + import base64 + from io import BytesIO + + def make_qr_base64(url): + qr = qrcode.make(url, box_size=5) + buf = BytesIO() + qr.save(buf, format='PNG') + return base64.b64encode(buf.getvalue()).decode() + + def replace_url_with_qr(match): + url = match.group(0) + try: + qr_b64 = make_qr_base64(url) + return f'
QR' + except: + return url + + url_pattern = re.compile(r'https?://[^\s<>"{}|\\^`\[\]]+') + html_content = url_pattern.sub(replace_url_with_qr, html_content) + # 支持 ReportLab 语法,转为 HTML import re html_content = re.sub( diff --git a/app/services/pdf_generator.py b/app/services/pdf_generator.py index 7dcb1ba..3f17a9f 100644 --- a/app/services/pdf_generator.py +++ b/app/services/pdf_generator.py @@ -53,6 +53,26 @@ try: except Exception as e: CHINESE_FONT_OK = False +import re +import qrcode +from io import BytesIO +from reportlab.platypus import Image as RLImage + +# URL 正则表达式 +URL_PATTERN = re.compile(r'https?://[^\s<>"{}|\\^`\[\]]+') + +def generate_qr_image(url, size=50*mm): + """生成二维码图片,返回 BytesIO 对象""" + qr = qrcode.make(url, box_size=10) + buf = BytesIO() + qr.save(buf, format='PNG') + buf.seek(0) + return buf + +def contains_url(text): + """检测文本是否包含 URL""" + return bool(URL_PATTERN.search(text)) + def md_to_xml(text): """将markdown转换为reportlab XML markup""" if not text: @@ -169,9 +189,27 @@ class PianoPDF: self.elements.append(Paragraph(md_to_xml(text), self.heading_style)) def add_paragraph(self, text): - if text: - self.elements.append(Paragraph(md_to_xml(text), self.body_style)) - self.elements.append(Spacer(1, 1*mm)) + if not text: + return + + # 检测是否是纯 URL + url_match = URL_PATTERN.match(text.strip()) + if url_match and url_match.group() == text.strip(): + # 纯 URL,生成二维码 + url = url_match.group() + try: + buf = generate_qr_image(url, 50*mm) + img = RLImage(buf, width=50*mm, height=50*mm) + img.hAlign = 'CENTER' + self.elements.append(img) + self.elements.append(Spacer(1, 2*mm)) + return + except Exception as e: + # QR生成失败,回退到文字 + pass + + self.elements.append(Paragraph(md_to_xml(text), self.body_style)) + self.elements.append(Spacer(1, 1*mm)) def add_list(self, items): for item in items: diff --git a/check_time.py b/check_time.py new file mode 100644 index 0000000..f5dfbb0 --- /dev/null +++ b/check_time.py @@ -0,0 +1,7 @@ +from app import create_app, db +from app.models import PracticePlan +app = create_app() +with app.app_context(): + plans = PracticePlan.query.order_by(db.desc("created_at")).limit(3).all() + for p in plans: + print(f"ID:{p.id} | created_at:{p.created_at} | student:{p.student.name if p.student else 'N/A'}") diff --git a/check_time_local.py b/check_time_local.py new file mode 100644 index 0000000..bcb54eb --- /dev/null +++ b/check_time_local.py @@ -0,0 +1,9 @@ +from app import create_app, db +from app.models import PracticePlan + +# Query local dev database (same data as production) +app = create_app() +with app.app_context(): + plans = PracticePlan.query.order_by(db.desc("created_at")).limit(5).all() + for p in plans: + print(f"ID:{p.id} | created_at:{p.created_at} | student:{p.student.name if p.student else 'N/A'}") diff --git a/requirements.txt b/requirements.txt index 2154b2e..8714aaa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ reportlab==4.0.7 Jinja2==3.1.3 requests==2.31.0 gunicorn==23.0.0 -markdown>=3.4 \ No newline at end of file +markdown>=3.4 +qrcode>=7.4 +Pillow>=10.0 \ No newline at end of file