Files
intelligence_system/applications/reporter/html_template.py
T
2025-10-30 09:54:47 +08:00

400 lines
10 KiB
Python

"""
HTML模板管理器
支持内置模板和外部HTML模板
"""
import os
import markdown
from bs4 import BeautifulSoup
import re
from typing import Optional
from loguru import logger
class HTMLTemplateManager:
"""HTML模板管理器"""
def __init__(self):
self.logger = logger.bind(module="HTMLTemplateManager")
def markdown_to_html(self, markdown_content: str) -> str:
"""将Markdown转换为HTML"""
html = markdown.markdown(
markdown_content,
extensions=['tables', 'fenced_code', 'codehilite']
)
return html
def render_builtin_template(self, markdown_content: str) -> str:
"""使用内置模板渲染HTML"""
html_body = self.markdown_to_html(markdown_content)
# 增强HTML结构
soup = BeautifulSoup(html_body, 'html.parser')
self._enhance_html_structure(soup)
# 生成完整HTML
html_template = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>汽车后市场情报报告</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root {{
--primary: #3498db;
--secondary: #2ecc71;
--accent: #e74c3c;
--dark: #2c3e50;
--light: #f8f9fa;
--border: #e0e0e0;
}}
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.8;
color: #333;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
}}
.report-container {{
max-width: 1200px;
margin: 0 auto;
padding: 40px;
background: white;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
border-radius: 12px;
}}
.report-header {{
text-align: center;
padding-bottom: 30px;
border-bottom: 3px solid var(--primary);
margin-bottom: 40px;
}}
.report-header h1 {{
color: var(--dark);
font-size: 2.5em;
margin-bottom: 10px;
}}
.report-header .report-date {{
color: #666;
font-size: 1.1em;
}}
h1 {{
color: var(--dark);
font-size: 2em;
margin: 30px 0 20px 0;
padding-bottom: 10px;
border-bottom: 2px solid var(--primary);
}}
h2 {{
color: var(--dark);
font-size: 1.6em;
margin: 25px 0 15px 0;
padding-left: 10px;
border-left: 4px solid var(--primary);
}}
h3 {{
color: var(--dark);
font-size: 1.3em;
margin: 20px 0 10px 0;
}}
h4 {{
color: #555;
font-size: 1.1em;
margin: 15px 0 8px 0;
}}
p {{
margin: 12px 0;
text-align: justify;
}}
ul, ol {{
margin: 15px 0;
padding-left: 30px;
}}
li {{
margin: 8px 0;
}}
/* 表格样式 */
table {{
width: 100%;
border-collapse: collapse;
margin: 25px 0;
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
border-radius: 8px;
overflow: hidden;
}}
table thead {{
background: linear-gradient(135deg, var(--primary) 0%, #2980b9 100%);
color: white;
}}
table th {{
padding: 15px;
text-align: left;
font-weight: 600;
}}
table td {{
padding: 12px 15px;
border-bottom: 1px solid var(--border);
}}
table tbody tr:hover {{
background-color: #f5f5f5;
}}
table tbody tr:last-child td {{
border-bottom: none;
}}
/* 代码块样式 */
pre {{
background: #f4f4f4;
border: 1px solid var(--border);
border-radius: 6px;
padding: 15px;
overflow-x: auto;
margin: 20px 0;
}}
code {{
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}}
pre code {{
background: none;
padding: 0;
}}
/* 链接样式 */
a {{
color: var(--primary);
text-decoration: none;
border-bottom: 1px dotted var(--primary);
transition: all 0.3s;
}}
a:hover {{
color: var(--accent);
border-bottom-color: var(--accent);
}}
/* 新闻列表样式 */
.news-item {{
background: #f9f9f9;
border-left: 4px solid var(--secondary);
padding: 15px 20px;
margin: 15px 0;
border-radius: 6px;
transition: all 0.3s;
}}
.news-item:hover {{
background: #f0f0f0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}}
.news-item h3 {{
margin-top: 0;
color: var(--dark);
}}
.news-item .news-meta {{
color: #666;
font-size: 0.9em;
margin-top: 10px;
}}
.news-item .news-category {{
display: inline-block;
background: var(--secondary);
color: white;
padding: 3px 10px;
border-radius: 12px;
font-size: 0.85em;
margin-right: 10px;
}}
/* 统计信息样式 */
.stats-box {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 10px;
margin: 25px 0;
}}
.stats-box h2 {{
color: white;
border: none;
padding: 0;
margin: 0 0 15px 0;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}}
.stat-item {{
text-align: center;
}}
.stat-number {{
font-size: 2.5em;
font-weight: bold;
margin-bottom: 5px;
}}
.stat-label {{
font-size: 0.9em;
opacity: 0.9;
}}
/* 响应式设计 */
@media (max-width: 768px) {{
.report-container {{
padding: 20px;
}}
.report-header h1 {{
font-size: 1.8em;
}}
h1 {{
font-size: 1.6em;
}}
h2 {{
font-size: 1.3em;
}}
table {{
font-size: 0.9em;
}}
table th,
table td {{
padding: 8px;
}}
}}
/* 打印样式 */
@media print {{
body {{
background: white;
padding: 0;
}}
.report-container {{
box-shadow: none;
padding: 0;
}}
}}
</style>
</head>
<body>
<div class="report-container">
{str(soup)}
</div>
</body>
</html>"""
return html_template
def render_external_template(self, template_path: str, markdown_content: str) -> str:
"""
使用外部HTML模板渲染
Args:
template_path: 外部模板文件路径
markdown_content: Markdown内容
Returns:
渲染后的HTML内容
"""
try:
with open(template_path, 'r', encoding='utf-8') as f:
template = f.read()
html_body = self.markdown_to_html(markdown_content)
# 查找模板中的占位符并替换
# 支持 {{content}} 或 {content} 等格式
patterns = [
r'\{\{content\}\}',
r'\{content\}',
r'<!--\s*content\s*-->',
]
replaced = False
for pattern in patterns:
if re.search(pattern, template, re.IGNORECASE):
template = re.sub(pattern, html_body, template, flags=re.IGNORECASE)
replaced = True
break
if not replaced:
# 如果没有找到占位符,在body标签内追加内容
soup = BeautifulSoup(template, 'html.parser')
body = soup.find('body')
if body:
body.append(BeautifulSoup(html_body, 'html.parser'))
else:
# 如果没有body标签,在html末尾追加
template += html_body
template = str(soup) if soup else template
self.logger.info(f"使用外部模板渲染: {template_path}")
return template
except Exception as e:
self.logger.error(f"使用外部模板失败: {str(e)},回退到内置模板", exc_info=True)
return self.render_builtin_template(markdown_content)
def _enhance_html_structure(self, soup: BeautifulSoup):
"""增强HTML结构"""
# 增强表格
for table in soup.find_all('table'):
if not table.get('class'):
table['class'] = 'data-table'
# 增强列表项
for ul in soup.find_all('ul'):
# 检查是否是新闻列表
if any('新闻' in str(item) for item in ul.find_all('li')):
ul['class'] = 'news-list'
# 增强链接
for a in soup.find_all('a'):
if not a.get('target'):
a['target'] = '_blank'
a['rel'] = 'noopener noreferrer'