mirror of
https://gitee.com/houhuan/TrendRadar.git
synced 2025-12-21 16:17:17 +08:00
v2.4.1
This commit is contained in:
parent
4ae1fc2910
commit
0c2f6fa065
289
main.py
289
main.py
@ -20,7 +20,7 @@ import requests
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
VERSION = "2.4.0"
|
VERSION = "2.4.1"
|
||||||
|
|
||||||
|
|
||||||
# === SMTP邮件配置 ===
|
# === SMTP邮件配置 ===
|
||||||
@ -1632,10 +1632,15 @@ def render_html_content(
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn {
|
.save-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
color: white;
|
color: white;
|
||||||
@ -1646,6 +1651,7 @@ def render_html_content(
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn:hover {
|
.save-btn:hover {
|
||||||
@ -1658,6 +1664,11 @@ def render_html_content(
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -2001,13 +2012,17 @@ def render_html_content(
|
|||||||
.news-item { gap: 8px; }
|
.news-item { gap: 8px; }
|
||||||
.new-item { gap: 8px; }
|
.new-item { gap: 8px; }
|
||||||
.news-number { width: 20px; height: 20px; font-size: 12px; }
|
.news-number { width: 20px; height: 20px; font-size: 12px; }
|
||||||
.save-btn {
|
.save-buttons {
|
||||||
position: static;
|
position: static;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
display: block;
|
display: flex;
|
||||||
width: fit-content;
|
gap: 8px;
|
||||||
margin-left: auto;
|
justify-content: center;
|
||||||
margin-right: auto;
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -2015,7 +2030,10 @@ def render_html_content(
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
<div class="save-buttons">
|
||||||
<button class="save-btn" onclick="saveAsImage()">保存为图片</button>
|
<button class="save-btn" onclick="saveAsImage()">保存为图片</button>
|
||||||
|
<button class="save-btn" onclick="saveAsMultipleImages()">分段保存</button>
|
||||||
|
</div>
|
||||||
<div class="header-title">热点新闻分析</div>
|
<div class="header-title">热点新闻分析</div>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@ -2267,7 +2285,7 @@ def render_html_content(
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function saveAsImage() {
|
async function saveAsImage() {
|
||||||
const button = document.querySelector('.save-btn');
|
const button = event.target;
|
||||||
const originalText = button.textContent;
|
const originalText = button.textContent;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -2279,17 +2297,14 @@ def render_html_content(
|
|||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
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));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
const container = document.querySelector('.container');
|
const container = document.querySelector('.container');
|
||||||
|
|
||||||
// 获取容器的精确位置和尺寸
|
|
||||||
const rect = container.getBoundingClientRect();
|
|
||||||
const computedStyle = window.getComputedStyle(container);
|
|
||||||
|
|
||||||
const canvas = await html2canvas(container, {
|
const canvas = await html2canvas(container, {
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
scale: 1.5,
|
scale: 1.5,
|
||||||
@ -2309,7 +2324,7 @@ def render_html_content(
|
|||||||
windowHeight: window.innerHeight
|
windowHeight: window.innerHeight
|
||||||
});
|
});
|
||||||
|
|
||||||
button.style.visibility = 'visible';
|
buttons.style.visibility = 'visible';
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -2330,7 +2345,239 @@ def render_html_content(
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
} catch (error) {
|
} 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 = '保存失败';
|
button.textContent = '保存失败';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.textContent = originalText;
|
button.textContent = originalText;
|
||||||
@ -3480,10 +3727,18 @@ def send_to_ntfy(
|
|||||||
mode: str = "daily",
|
mode: str = "daily",
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""发送到ntfy(支持分批发送,严格遵守4KB限制)"""
|
"""发送到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 = {
|
headers = {
|
||||||
"Content-Type": "text/plain; charset=utf-8",
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
"Markdown": "yes",
|
"Markdown": "yes",
|
||||||
"Title": f"TrendRadar 热点分析报告 - {report_type}",
|
"Title": f"TrendRadar Report - {report_type_en}",
|
||||||
"Priority": "default",
|
"Priority": "default",
|
||||||
"Tags": "newspaper,📰",
|
"Tags": "newspaper,📰",
|
||||||
}
|
}
|
||||||
@ -3526,7 +3781,7 @@ def send_to_ntfy(
|
|||||||
batch_header = f"**[第 {i}/{len(batches)} 批次]**\n\n"
|
batch_header = f"**[第 {i}/{len(batches)} 批次]**\n\n"
|
||||||
batch_content = batch_header + batch_content
|
batch_content = batch_header + batch_content
|
||||||
current_headers["Title"] = (
|
current_headers["Title"] = (
|
||||||
f"TrendRadar 热点分析报告 - {report_type} ({i}/{len(batches)})"
|
f"TrendRadar Report - {report_type_en} ({i}/{len(batches)})"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
15
readme.md
15
readme.md
@ -11,7 +11,7 @@
|
|||||||
[](https://github.com/sansan0/TrendRadar/stargazers)
|
[](https://github.com/sansan0/TrendRadar/stargazers)
|
||||||
[](https://github.com/sansan0/TrendRadar/network/members)
|
[](https://github.com/sansan0/TrendRadar/network/members)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/sansan0/TrendRadar)
|
[](https://github.com/sansan0/TrendRadar)
|
||||||
|
|
||||||
[](https://work.weixin.qq.com/)
|
[](https://work.weixin.qq.com/)
|
||||||
[](https://telegram.org/)
|
[](https://telegram.org/)
|
||||||
@ -119,6 +119,8 @@ platforms:
|
|||||||
name: "华尔街见闻"
|
name: "华尔街见闻"
|
||||||
# 添加更多平台...
|
# 添加更多平台...
|
||||||
```
|
```
|
||||||
|
如果不会看的话,就直接复制他人整理好的部分[平台配置](https://github.com/sansan0/TrendRadar/issues/95)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### **智能推送策略**
|
### **智能推送策略**
|
||||||
@ -462,6 +464,15 @@ GitHub 一键 Fork 即可使用,无需编程基础。
|
|||||||
- **小版本更新**:从 v2.x 升级到 v2.y, 用本项目的 `main.py` 代码替换你 fork 仓库中的对应文件
|
- **小版本更新**:从 v2.x 升级到 v2.y, 用本项目的 `main.py` 代码替换你 fork 仓库中的对应文件
|
||||||
- **大版本升级**:从 v1.x 升级到 v2.y, 建议删除现有 fork 后重新 fork,这样更省力且避免配置冲突
|
- **大版本升级**:从 v1.x 升级到 v2.y, 建议删除现有 fork 后重新 fork,这样更省力且避免配置冲突
|
||||||
|
|
||||||
|
### 2025/10/8 - v2.4.1
|
||||||
|
|
||||||
|
- 修复 ntfy 推送编码问题
|
||||||
|
- 增加 github page 图片分段导出功能
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>👉 历史更新</strong></summary>
|
||||||
|
|
||||||
### 2025/10/2 - v2.4.0
|
### 2025/10/2 - v2.4.0
|
||||||
|
|
||||||
**新增 ntfy 推送通知**
|
**新增 ntfy 推送通知**
|
||||||
@ -478,8 +489,6 @@ GitHub 一键 Fork 即可使用,无需编程基础。
|
|||||||
- **更新提示**:
|
- **更新提示**:
|
||||||
- 建议使用【大版本更新】
|
- 建议使用【大版本更新】
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>👉 历史更新</strong></summary>
|
|
||||||
|
|
||||||
### 2025/09/26 - v2.3.2
|
### 2025/09/26 - v2.3.2
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user