|
|
@@ -20,7 +20,7 @@ import requests
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
-VERSION = "2.4.0"
|
|
|
+VERSION = "2.4.1"
|
|
|
|
|
|
|
|
|
# === SMTP邮件配置 ===
|
|
|
@@ -1632,10 +1632,15 @@ def render_html_content(
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
- .save-btn {
|
|
|
+ .save-buttons {
|
|
|
position: absolute;
|
|
|
top: 16px;
|
|
|
right: 16px;
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .save-btn {
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
|
color: white;
|
|
|
@@ -1646,6 +1651,7 @@ def render_html_content(
|
|
|
font-weight: 500;
|
|
|
transition: all 0.2s ease;
|
|
|
backdrop-filter: blur(10px);
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.save-btn:hover {
|
|
|
@@ -1658,6 +1664,11 @@ def render_html_content(
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
|
|
|
+ .save-btn:disabled {
|
|
|
+ opacity: 0.6;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+
|
|
|
.header-title {
|
|
|
font-size: 22px;
|
|
|
font-weight: 700;
|
|
|
@@ -2001,13 +2012,17 @@ def render_html_content(
|
|
|
.news-item { gap: 8px; }
|
|
|
.new-item { gap: 8px; }
|
|
|
.news-number { width: 20px; height: 20px; font-size: 12px; }
|
|
|
- .save-btn {
|
|
|
+ .save-buttons {
|
|
|
position: static;
|
|
|
margin-bottom: 16px;
|
|
|
- display: block;
|
|
|
- width: fit-content;
|
|
|
- margin-left: auto;
|
|
|
- margin-right: auto;
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ justify-content: center;
|
|
|
+ flex-direction: column;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+ .save-btn {
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
@@ -2015,7 +2030,10 @@ def render_html_content(
|
|
|
<body>
|
|
|
<div class="container">
|
|
|
<div class="header">
|
|
|
- <button class="save-btn" onclick="saveAsImage()">保存为图片</button>
|
|
|
+ <div class="save-buttons">
|
|
|
+ <button class="save-btn" onclick="saveAsImage()">保存为图片</button>
|
|
|
+ <button class="save-btn" onclick="saveAsMultipleImages()">分段保存</button>
|
|
|
+ </div>
|
|
|
<div class="header-title">热点新闻分析</div>
|
|
|
<div class="header-info">
|
|
|
<div class="info-item">
|
|
|
@@ -2267,7 +2285,7 @@ def render_html_content(
|
|
|
|
|
|
<script>
|
|
|
async function saveAsImage() {
|
|
|
- const button = document.querySelector('.save-btn');
|
|
|
+ const button = event.target;
|
|
|
const originalText = button.textContent;
|
|
|
|
|
|
try {
|
|
|
@@ -2279,17 +2297,14 @@ def render_html_content(
|
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
|
|
// 截图前隐藏按钮
|
|
|
- button.style.visibility = 'hidden';
|
|
|
+ const buttons = document.querySelector('.save-buttons');
|
|
|
+ buttons.style.visibility = 'hidden';
|
|
|
|
|
|
// 再次等待确保按钮完全隐藏
|
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
|
|
const container = document.querySelector('.container');
|
|
|
|
|
|
- // 获取容器的精确位置和尺寸
|
|
|
- const rect = container.getBoundingClientRect();
|
|
|
- const computedStyle = window.getComputedStyle(container);
|
|
|
-
|
|
|
const canvas = await html2canvas(container, {
|
|
|
backgroundColor: '#ffffff',
|
|
|
scale: 1.5,
|
|
|
@@ -2309,7 +2324,7 @@ def render_html_content(
|
|
|
windowHeight: window.innerHeight
|
|
|
});
|
|
|
|
|
|
- button.style.visibility = 'visible';
|
|
|
+ buttons.style.visibility = 'visible';
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
const now = new Date();
|
|
|
@@ -2330,7 +2345,239 @@ def render_html_content(
|
|
|
}, 2000);
|
|
|
|
|
|
} catch (error) {
|
|
|
- button.style.visibility = 'visible';
|
|
|
+ const buttons = document.querySelector('.save-buttons');
|
|
|
+ buttons.style.visibility = 'visible';
|
|
|
+ button.textContent = '保存失败';
|
|
|
+ setTimeout(() => {
|
|
|
+ button.textContent = originalText;
|
|
|
+ button.disabled = false;
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function saveAsMultipleImages() {
|
|
|
+ const button = event.target;
|
|
|
+ const originalText = button.textContent;
|
|
|
+ const container = document.querySelector('.container');
|
|
|
+ const scale = 1.5;
|
|
|
+ const maxHeight = 5000 / scale;
|
|
|
+
|
|
|
+ try {
|
|
|
+ button.textContent = '分析中...';
|
|
|
+ button.disabled = true;
|
|
|
+
|
|
|
+ // 获取所有可能的分割元素
|
|
|
+ const newsItems = Array.from(container.querySelectorAll('.news-item'));
|
|
|
+ const wordGroups = Array.from(container.querySelectorAll('.word-group'));
|
|
|
+ const newSection = container.querySelector('.new-section');
|
|
|
+ const errorSection = container.querySelector('.error-section');
|
|
|
+ const header = container.querySelector('.header');
|
|
|
+ const footer = container.querySelector('.footer');
|
|
|
+
|
|
|
+ // 计算元素位置和高度
|
|
|
+ const containerRect = container.getBoundingClientRect();
|
|
|
+ const elements = [];
|
|
|
+
|
|
|
+ // 添加header作为必须包含的元素
|
|
|
+ elements.push({
|
|
|
+ type: 'header',
|
|
|
+ element: header,
|
|
|
+ top: 0,
|
|
|
+ bottom: header.offsetHeight,
|
|
|
+ height: header.offsetHeight
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加错误信息(如果存在)
|
|
|
+ if (errorSection) {
|
|
|
+ const rect = errorSection.getBoundingClientRect();
|
|
|
+ elements.push({
|
|
|
+ type: 'error',
|
|
|
+ element: errorSection,
|
|
|
+ top: rect.top - containerRect.top,
|
|
|
+ bottom: rect.bottom - containerRect.top,
|
|
|
+ height: rect.height
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按word-group分组处理news-item
|
|
|
+ wordGroups.forEach(group => {
|
|
|
+ const groupRect = group.getBoundingClientRect();
|
|
|
+ const groupNewsItems = group.querySelectorAll('.news-item');
|
|
|
+
|
|
|
+ // 添加word-group的header部分
|
|
|
+ const wordHeader = group.querySelector('.word-header');
|
|
|
+ if (wordHeader) {
|
|
|
+ const headerRect = wordHeader.getBoundingClientRect();
|
|
|
+ elements.push({
|
|
|
+ type: 'word-header',
|
|
|
+ element: wordHeader,
|
|
|
+ parent: group,
|
|
|
+ top: groupRect.top - containerRect.top,
|
|
|
+ bottom: headerRect.bottom - containerRect.top,
|
|
|
+ height: headerRect.height
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加每个news-item
|
|
|
+ groupNewsItems.forEach(item => {
|
|
|
+ const rect = item.getBoundingClientRect();
|
|
|
+ elements.push({
|
|
|
+ type: 'news-item',
|
|
|
+ element: item,
|
|
|
+ parent: group,
|
|
|
+ top: rect.top - containerRect.top,
|
|
|
+ bottom: rect.bottom - containerRect.top,
|
|
|
+ height: rect.height
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加新增新闻部分
|
|
|
+ if (newSection) {
|
|
|
+ const rect = newSection.getBoundingClientRect();
|
|
|
+ elements.push({
|
|
|
+ type: 'new-section',
|
|
|
+ element: newSection,
|
|
|
+ top: rect.top - containerRect.top,
|
|
|
+ bottom: rect.bottom - containerRect.top,
|
|
|
+ height: rect.height
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加footer
|
|
|
+ const footerRect = footer.getBoundingClientRect();
|
|
|
+ elements.push({
|
|
|
+ type: 'footer',
|
|
|
+ element: footer,
|
|
|
+ top: footerRect.top - containerRect.top,
|
|
|
+ bottom: footerRect.bottom - containerRect.top,
|
|
|
+ height: footer.offsetHeight
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算分割点
|
|
|
+ const segments = [];
|
|
|
+ let currentSegment = { start: 0, end: 0, height: 0, includeHeader: true };
|
|
|
+ let headerHeight = header.offsetHeight;
|
|
|
+ currentSegment.height = headerHeight;
|
|
|
+
|
|
|
+ for (let i = 1; i < elements.length; i++) {
|
|
|
+ const element = elements[i];
|
|
|
+ const potentialHeight = element.bottom - currentSegment.start;
|
|
|
+
|
|
|
+ // 检查是否需要创建新分段
|
|
|
+ if (potentialHeight > maxHeight && currentSegment.height > headerHeight) {
|
|
|
+ // 在前一个元素结束处分割
|
|
|
+ currentSegment.end = elements[i - 1].bottom;
|
|
|
+ segments.push(currentSegment);
|
|
|
+
|
|
|
+ // 开始新分段
|
|
|
+ currentSegment = {
|
|
|
+ start: currentSegment.end,
|
|
|
+ end: 0,
|
|
|
+ height: element.bottom - currentSegment.end,
|
|
|
+ includeHeader: false
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ currentSegment.height = potentialHeight;
|
|
|
+ currentSegment.end = element.bottom;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加最后一个分段
|
|
|
+ if (currentSegment.height > 0) {
|
|
|
+ currentSegment.end = container.offsetHeight;
|
|
|
+ segments.push(currentSegment);
|
|
|
+ }
|
|
|
+
|
|
|
+ button.textContent = `生成中 (0/${segments.length})...`;
|
|
|
+
|
|
|
+ // 隐藏保存按钮
|
|
|
+ const buttons = document.querySelector('.save-buttons');
|
|
|
+ buttons.style.visibility = 'hidden';
|
|
|
+
|
|
|
+ // 为每个分段生成图片
|
|
|
+ const images = [];
|
|
|
+ for (let i = 0; i < segments.length; i++) {
|
|
|
+ const segment = segments[i];
|
|
|
+ button.textContent = `生成中 (${i + 1}/${segments.length})...`;
|
|
|
+
|
|
|
+ // 创建临时容器用于截图
|
|
|
+ const tempContainer = document.createElement('div');
|
|
|
+ tempContainer.style.cssText = `
|
|
|
+ position: absolute;
|
|
|
+ left: -9999px;
|
|
|
+ top: 0;
|
|
|
+ width: ${container.offsetWidth}px;
|
|
|
+ background: white;
|
|
|
+ `;
|
|
|
+ tempContainer.className = 'container';
|
|
|
+
|
|
|
+ // 克隆容器内容
|
|
|
+ const clonedContainer = container.cloneNode(true);
|
|
|
+
|
|
|
+ // 移除克隆内容中的保存按钮
|
|
|
+ const clonedButtons = clonedContainer.querySelector('.save-buttons');
|
|
|
+ if (clonedButtons) {
|
|
|
+ clonedButtons.style.display = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ tempContainer.appendChild(clonedContainer);
|
|
|
+ document.body.appendChild(tempContainer);
|
|
|
+
|
|
|
+ // 等待DOM更新
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
+
|
|
|
+ // 使用html2canvas截取特定区域
|
|
|
+ const canvas = await html2canvas(clonedContainer, {
|
|
|
+ backgroundColor: '#ffffff',
|
|
|
+ scale: scale,
|
|
|
+ useCORS: true,
|
|
|
+ allowTaint: false,
|
|
|
+ imageTimeout: 10000,
|
|
|
+ logging: false,
|
|
|
+ width: container.offsetWidth,
|
|
|
+ height: segment.end - segment.start,
|
|
|
+ x: 0,
|
|
|
+ y: segment.start,
|
|
|
+ windowWidth: window.innerWidth,
|
|
|
+ windowHeight: window.innerHeight
|
|
|
+ });
|
|
|
+
|
|
|
+ images.push(canvas.toDataURL('image/png', 1.0));
|
|
|
+
|
|
|
+ // 清理临时容器
|
|
|
+ document.body.removeChild(tempContainer);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 恢复按钮显示
|
|
|
+ buttons.style.visibility = 'visible';
|
|
|
+
|
|
|
+ // 下载所有图片
|
|
|
+ const now = new Date();
|
|
|
+ const baseFilename = `TrendRadar_热点新闻分析_${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}`;
|
|
|
+
|
|
|
+ for (let i = 0; i < images.length; i++) {
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.download = `${baseFilename}_part${i + 1}.png`;
|
|
|
+ link.href = images[i];
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+
|
|
|
+ // 延迟一下避免浏览器阻止多个下载
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
+ }
|
|
|
+
|
|
|
+ button.textContent = `已保存 ${segments.length} 张图片!`;
|
|
|
+ setTimeout(() => {
|
|
|
+ button.textContent = originalText;
|
|
|
+ button.disabled = false;
|
|
|
+ }, 2000);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('分段保存失败:', error);
|
|
|
+ const buttons = document.querySelector('.save-buttons');
|
|
|
+ buttons.style.visibility = 'visible';
|
|
|
button.textContent = '保存失败';
|
|
|
setTimeout(() => {
|
|
|
button.textContent = originalText;
|
|
|
@@ -3480,10 +3727,18 @@ def send_to_ntfy(
|
|
|
mode: str = "daily",
|
|
|
) -> bool:
|
|
|
"""发送到ntfy(支持分批发送,严格遵守4KB限制)"""
|
|
|
+ # 避免 HTTP header 编码问题
|
|
|
+ report_type_en_map = {
|
|
|
+ "当日汇总": "Daily Summary",
|
|
|
+ "当前榜单汇总": "Current Ranking",
|
|
|
+ "增量更新": "Incremental Update",
|
|
|
+ }
|
|
|
+ report_type_en = report_type_en_map.get(report_type, report_type)
|
|
|
+
|
|
|
headers = {
|
|
|
"Content-Type": "text/plain; charset=utf-8",
|
|
|
"Markdown": "yes",
|
|
|
- "Title": f"TrendRadar 热点分析报告 - {report_type}",
|
|
|
+ "Title": f"TrendRadar Report - {report_type_en}",
|
|
|
"Priority": "default",
|
|
|
"Tags": "newspaper,📰",
|
|
|
}
|
|
|
@@ -3526,7 +3781,7 @@ def send_to_ntfy(
|
|
|
batch_header = f"**[第 {i}/{len(batches)} 批次]**\n\n"
|
|
|
batch_content = batch_header + batch_content
|
|
|
current_headers["Title"] = (
|
|
|
- f"TrendRadar 热点分析报告 - {report_type} ({i}/{len(batches)})"
|
|
|
+ f"TrendRadar Report - {report_type_en} ({i}/{len(batches)})"
|
|
|
)
|
|
|
|
|
|
try:
|