400 lines
10 KiB
Python
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'
|
|
|