From 0def77dc307948ece8c145ec1de7e30574ae48f3 Mon Sep 17 00:00:00 2001 From: auto-bot Date: Wed, 24 Dec 2025 10:58:43 +0800 Subject: [PATCH] feat: add /admin/templates/preview endpoint; add preview button to template editor --- app/admin.py | 28 ++++++++++++++++++++++++- templates/admin/templates.html | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/app/admin.py b/app/admin.py index de45752..c75a7fb 100644 --- a/app/admin.py +++ b/app/admin.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Request, Form, Depends, HTTPException +from fastapi import APIRouter, Request, Form, Depends, HTTPException, Body from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session, joinedload, aliased @@ -7,6 +7,7 @@ import json from app.db import SessionLocal, Target, NotificationChannel, MessageTemplate, WebhookEndpoint, RequestLog, DeliveryLog, ProcessingRule, RuleAction from app.services.stats import stats_service from app.services.engine import engine +from app.templates import safe_render router = APIRouter(prefix="/admin", tags=["admin"]) templates = Jinja2Templates(directory="templates") @@ -424,6 +425,31 @@ async def delete_template(id: int = Form(...), db: Session = Depends(get_db)): db.commit() return RedirectResponse(url="/admin/templates", status_code=303) + +@router.post("/templates/preview") +async def preview_template(data: dict = Body(...), db: Session = Depends(get_db)): + """ + Preview a template with an optional sample payload. + Request JSON: { "template_content": "...", "sample_payload": {...}, "vars": {...} } + """ + template_content = data.get("template_content") + if not template_content: + return JSONResponse({"error": "template_content is required"}, status_code=400) + + sample_payload = data.get("sample_payload") or {} + # Build render context using engine's flatten helper if available + try: + render_context = engine._flatten_payload(sample_payload) if hasattr(engine, "_flatten_payload") else dict(sample_payload) + # Merge any provided template vars + extra_vars = data.get("vars") or {} + if isinstance(extra_vars, dict): + render_context.update(extra_vars) + + rendered = safe_render(template_content, render_context) + return JSONResponse({"rendered": rendered}) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=400) + # --- Logs --- @router.get("/logs", response_class=HTMLResponse) async def list_logs(request: Request, db: Session = Depends(get_db)): diff --git a/templates/admin/templates.html b/templates/admin/templates.html index ff2279c..618e2e3 100644 --- a/templates/admin/templates.html +++ b/templates/admin/templates.html @@ -67,9 +67,14 @@ +
+ + +
@@ -88,6 +93,7 @@ document.getElementById('templateId').value = ""; document.getElementById('templateName').value = ""; document.getElementById('templateContent').value = ""; + document.getElementById('templateSample').value = ''; modal.show(); } @@ -97,7 +103,38 @@ document.getElementById('templateId').value = id; document.getElementById('templateName').value = name; document.getElementById('templateContent').value = content; + document.getElementById('templateSample').value = ''; modal.show(); } + + async function previewTemplate() { + const content = document.getElementById('templateContent').value; + let sample = {}; + try { + const sampleText = document.getElementById('templateSample').value; + if (sampleText && sampleText.trim()) { + sample = JSON.parse(sampleText); + } + } catch (e) { + alert('示例 payload JSON 格式错误: ' + e); + return; + } + + try { + const res = await fetch('/admin/templates/preview', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ template_content: content, sample_payload: sample }) + }); + const data = await res.json(); + if (res.ok) { + alert('渲染结果:\\n' + data.rendered); + } else { + alert('预览失败: ' + (data.error || '未知错误')); + } + } catch (e) { + alert('请求错误: ' + e); + } + } {% endblock %}