生成日报、周报
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user