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'
'
+ 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