sansan 9 luni în urmă
părinte
comite
ac27c47fcf
3 a modificat fișierele cu 546 adăugiri și 107 ștergeri
  1. 534 100
      main.py
  2. 11 6
      readme.md
  3. 1 1
      version

+ 534 - 100
main.py

@@ -15,7 +15,7 @@ import requests
 import yaml
 
 
-VERSION = "2.0.2"
+VERSION = "2.0.3"
 
 
 # === 配置管理 ===
@@ -1410,142 +1410,576 @@ def render_html_content(
     <html>
     <head>
         <meta charset="UTF-8">
-        <title>频率词统计报告</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <title>热点新闻分析</title>
         <style>
-            body { font-family: Arial, sans-serif; margin: 20px; }
-            h1, h2 { color: #333; }
-            table { border-collapse: collapse; width: 100%; margin-top: 20px; }
-            th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
-            th { background-color: #f2f2f2; }
-            tr:nth-child(even) { background-color: #f9f9f9; }
-            .word { font-weight: bold; }
-            .count { text-align: center; }
-            .percentage { text-align: center; }
-            .titles { max-width: 500px; }
-            .source { color: #666; font-style: italic; }
-            .error { color: #d9534f; }
-            .news-link { 
-                color: #007bff; 
-                text-decoration: none; 
-                border-bottom: 1px dotted #007bff;
+            * { box-sizing: border-box; }
+            body { 
+                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
+                margin: 0; 
+                padding: 16px; 
+                background: #fafafa;
+                color: #333;
+                line-height: 1.5;
             }
-            .news-link:hover { 
-                color: #0056b3; 
-                text-decoration: underline; 
+            
+            .container {
+                max-width: 600px;
+                margin: 0 auto;
+                background: white;
+                border-radius: 12px;
+                overflow: hidden;
+                box-shadow: 0 2px 16px rgba(0,0,0,0.06);
             }
-            .news-link:visited { 
-                color: #6f42c1; 
+            
+            .header {
+                background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
+                color: white;
+                padding: 32px 24px;
+                text-align: center;
             }
-            .no-link { 
-                color: #333; 
+            
+            .header-title {
+                font-size: 22px;
+                font-weight: 700;
+                margin: 0 0 20px 0;
             }
-            .new-title {
-                background-color: #fff3cd;
-                border: 1px solid #ffc107;
-                border-radius: 3px;
+            
+            .header-info {
+                display: grid;
+                grid-template-columns: 1fr 1fr;
+                gap: 16px;
+                font-size: 14px;
+                opacity: 0.95;
+            }
+            
+            .info-item {
+                text-align: center;
+            }
+            
+            .info-label {
+                display: block;
+                font-size: 12px;
+                opacity: 0.8;
+                margin-bottom: 4px;
+            }
+            
+            .info-value {
+                font-weight: 600;
+                font-size: 16px;
+            }
+            
+            .content {
+                padding: 24px;
+            }
+            
+            .word-group {
+                margin-bottom: 40px;
+            }
+            
+            .word-group:first-child {
+                margin-top: 0;
+            }
+            
+            .word-header {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                margin-bottom: 20px;
+                padding-bottom: 8px;
+                border-bottom: 1px solid #f0f0f0;
+            }
+            
+            .word-info {
+                display: flex;
+                align-items: center;
+                gap: 12px;
+            }
+            
+            .word-name {
+                font-size: 17px;
+                font-weight: 600;
+                color: #1a1a1a;
+            }
+            
+            .word-count {
+                color: #666;
+                font-size: 13px;
+                font-weight: 500;
+            }
+            
+            .word-count.hot { color: #dc2626; font-weight: 600; }
+            .word-count.warm { color: #ea580c; font-weight: 600; }
+            
+            .word-index {
+                color: #999;
+                font-size: 12px;
+            }
+            
+            .news-item {
+                margin-bottom: 20px;
+                padding: 16px 0;
+                border-bottom: 1px solid #f5f5f5;
+                position: relative;
+                display: flex;
+                gap: 12px;
+                align-items: center;
+            }
+            
+            .news-item:last-child {
+                border-bottom: none;
+            }
+            
+            .news-item.new::after {
+                content: "NEW";
+                position: absolute;
+                top: 12px;
+                right: 0;
+                background: #fbbf24;
+                color: #92400e;
+                font-size: 9px;
+                font-weight: 700;
+                padding: 3px 6px;
+                border-radius: 4px;
+                letter-spacing: 0.5px;
+            }
+            
+            .news-number {
+                color: #999;
+                font-size: 13px;
+                font-weight: 600;
+                min-width: 20px;
+                text-align: center;
+                flex-shrink: 0;
+                background: #f8f9fa;
+                border-radius: 50%;
+                width: 24px;
+                height: 24px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                align-self: flex-start;
+                margin-top: 8px;
+            }
+            
+            .news-content {
+                flex: 1;
+                min-width: 0;
+                padding-right: 40px;
+            }
+            
+            .news-item.new .news-content {
+                padding-right: 50px;
+            }
+            
+            .news-header {
+                display: flex;
+                align-items: center;
+                gap: 8px;
+                margin-bottom: 8px;
+                flex-wrap: wrap;
+            }
+            
+            .source-name {
+                color: #666;
+                font-size: 12px;
+                font-weight: 500;
+            }
+            
+            .rank-num {
+                color: #fff;
+                background: #6b7280;
+                font-size: 10px;
+                font-weight: 700;
                 padding: 2px 6px;
-                margin: 2px 0;
+                border-radius: 10px;
+                min-width: 18px;
+                text-align: center;
+            }
+            
+            .rank-num.top { background: #dc2626; }
+            .rank-num.high { background: #ea580c; }
+            
+            .time-info {
+                color: #999;
+                font-size: 11px;
+            }
+            
+            .count-info {
+                color: #059669;
+                font-size: 11px;
+                font-weight: 500;
+            }
+            
+            .news-title {
+                font-size: 15px;
+                line-height: 1.4;
+                color: #1a1a1a;
+                margin: 0;
+            }
+            
+            .news-link {
+                color: #2563eb;
+                text-decoration: none;
+            }
+            
+            .news-link:hover {
+                text-decoration: underline;
+            }
+            
+            .news-link:visited {
+                color: #7c3aed;
             }
+            
             .new-section {
-                background-color: #d1ecf1;
-                border: 1px solid #bee5eb;
-                border-radius: 5px;
-                padding: 10px;
-                margin-top: 10px;
+                margin-top: 40px;
+                padding-top: 24px;
+                border-top: 2px solid #f0f0f0;
             }
-            .new-section h3 {
-                color: #0c5460;
-                margin-top: 0;
+            
+            .new-section-title {
+                color: #1a1a1a;
+                font-size: 16px;
+                font-weight: 600;
+                margin: 0 0 20px 0;
+            }
+            
+            .new-source-group {
+                margin-bottom: 24px;
+            }
+            
+            .new-source-title {
+                color: #666;
+                font-size: 13px;
+                font-weight: 500;
+                margin: 0 0 12px 0;
+                padding-bottom: 6px;
+                border-bottom: 1px solid #f5f5f5;
+            }
+            
+            .new-item {
+                display: flex;
+                align-items: center;
+                gap: 12px;
+                padding: 8px 0;
+                border-bottom: 1px solid #f9f9f9;
+            }
+            
+            .new-item:last-child {
+                border-bottom: none;
+            }
+            
+            .new-item-number {
+                color: #999;
+                font-size: 12px;
+                font-weight: 600;
+                min-width: 18px;
+                text-align: center;
+                flex-shrink: 0;
+                background: #f8f9fa;
+                border-radius: 50%;
+                width: 20px;
+                height: 20px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+            }
+            
+            .new-item-rank {
+                color: #fff;
+                background: #6b7280;
+                font-size: 10px;
+                font-weight: 700;
+                padding: 3px 6px;
+                border-radius: 8px;
+                min-width: 20px;
+                text-align: center;
+                flex-shrink: 0;
+            }
+            
+            .new-item-rank.top { background: #dc2626; }
+            .new-item-rank.high { background: #ea580c; }
+            
+            .new-item-content {
+                flex: 1;
+                min-width: 0;
+            }
+            
+            .new-item-title {
+                font-size: 14px;
+                line-height: 1.4;
+                color: #1a1a1a;
+                margin: 0;
+            }
+            
+            .error-section {
+                background: #fef2f2;
+                border: 1px solid #fecaca;
+                border-radius: 8px;
+                padding: 16px;
+                margin-bottom: 24px;
+            }
+            
+            .error-title {
+                color: #dc2626;
+                font-size: 14px;
+                font-weight: 600;
+                margin: 0 0 8px 0;
+            }
+            
+            .error-list {
+                list-style: none;
+                padding: 0;
+                margin: 0;
+            }
+            
+            .error-item {
+                color: #991b1b;
+                font-size: 13px;
+                padding: 2px 0;
+                font-family: 'SF Mono', Consolas, monospace;
+            }
+            
+            @media (max-width: 480px) {
+                body { padding: 12px; }
+                .header { padding: 24px 20px; }
+                .content { padding: 20px; }
+                .header-info { grid-template-columns: 1fr; gap: 12px; }
+                .news-header { gap: 6px; }
+                .news-content { padding-right: 45px; }
+                .news-item { gap: 8px; }
+                .new-item { gap: 8px; }
+                .news-number { width: 20px; height: 20px; font-size: 12px; }
             }
         </style>
     </head>
     <body>
-        <h1>频率词统计报告</h1>
-    """
-
+        <div class="container">
+            <div class="header">
+                <div class="header-title">热点新闻分析</div>
+                <div class="header-info">
+                    <div class="info-item">
+                        <span class="info-label">报告类型</span>
+                        <span class="info-value">"""
+
+    # 处理报告类型显示
     if is_daily_summary:
         if mode == "current":
-            html += "<p>报告类型: 当前榜单模式</p>"
+            html += "当前榜单"
         elif mode == "incremental":
-            html += "<p>报告类型: 增量模式</p>"
+            html += "增量模式"
         else:
-            html += "<p>报告类型: 当日汇总</p>"
+            html += "当日汇总"
     else:
-        html += "<p>报告类型: 实时分析</p>"
+        html += "实时分析"
+
+    html += """</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="info-label">新闻总数</span>
+                        <span class="info-value">"""
+    
+    html += f"{total_titles} 条"
+    
+    # 计算筛选后的热点新闻数量
+    hot_news_count = sum(len(stat["titles"]) for stat in report_data["stats"])
+    
+    html += """</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="info-label">热点新闻</span>
+                        <span class="info-value">"""
+    
+    html += f"{hot_news_count} 条"
+
+    html += """</span>
+                    </div>
+                    <div class="info-item">
+                        <span class="info-label">生成时间</span>
+                        <span class="info-value">"""
 
     now = get_beijing_time()
-    html += f"<p>总标题数: {total_titles}</p>"
-    html += f"<p>生成时间: {now.strftime('%Y-%m-%d %H:%M:%S')}</p>"
+    html += now.strftime('%m-%d %H:%M')
 
+    html += """</span>
+                    </div>
+                </div>
+            </div>
+            
+            <div class="content">"""
+
+    # 处理失败ID错误信息
     if report_data["failed_ids"]:
         html += """
-        <div class="error">
-            <h2>请求失败的平台</h2>
-            <ul>
-        """
+                <div class="error-section">
+                    <div class="error-title">⚠️ 请求失败的平台</div>
+                    <ul class="error-list">"""
         for id_value in report_data["failed_ids"]:
-            html += f"<li>{html_escape(id_value)}</li>"
+            html += f'<li class="error-item">{html_escape(id_value)}</li>'
         html += """
-            </ul>
-        </div>
-        """
+                    </ul>
+                </div>"""
 
-    html += """
-        <table>
-            <tr>
-                <th>排名</th>
-                <th>频率词</th>
-                <th>出现次数</th>
-                <th>占比</th>
-                <th>相关标题</th>
-            </tr>
-    """
-
-    for i, stat in enumerate(report_data["stats"], 1):
-        formatted_titles = []
-
-        for title_data in stat["titles"]:
-            formatted_title = format_title_for_platform("html", title_data)
-            formatted_titles.append(formatted_title)
-
-        escaped_word = html_escape(stat["word"])
-        html += f"""
-            <tr>
-                <td>{i}</td>
-                <td class="word">{escaped_word}</td>
-                <td class="count">{stat['count']}</td>
-                <td class="percentage">{stat.get('percentage', 0)}%</td>
-                <td class="titles">{"<br>".join(formatted_titles)}</td>
-            </tr>
-        """
+    # 处理主要统计数据
+    if report_data["stats"]:
+        total_count = len(report_data["stats"])
+        
+        for i, stat in enumerate(report_data["stats"], 1):
+            count = stat["count"]
+            
+            # 确定热度等级
+            if count >= 10:
+                count_class = "hot"
+            elif count >= 5:
+                count_class = "warm"
+            else:
+                count_class = ""
+
+            escaped_word = html_escape(stat["word"])
+            
+            html += f"""
+                <div class="word-group">
+                    <div class="word-header">
+                        <div class="word-info">
+                            <div class="word-name">{escaped_word}</div>
+                            <div class="word-count {count_class}">{count} 条</div>
+                        </div>
+                        <div class="word-index">{i}/{total_count}</div>
+                    </div>"""
+
+            # 处理每个词组下的新闻标题,给每条新闻标上序号
+            for j, title_data in enumerate(stat["titles"], 1):
+                is_new = title_data.get("is_new", False)
+                new_class = "new" if is_new else ""
+                
+                html += f"""
+                    <div class="news-item {new_class}">
+                        <div class="news-number">{j}</div>
+                        <div class="news-content">
+                            <div class="news-header">
+                                <span class="source-name">{html_escape(title_data["source_name"])}</span>"""
+                
+                # 处理排名显示
+                ranks = title_data.get("ranks", [])
+                if ranks:
+                    min_rank = min(ranks)
+                    max_rank = max(ranks)
+                    rank_threshold = title_data.get("rank_threshold", 10)
+                    
+                    # 确定排名等级
+                    if min_rank <= 3:
+                        rank_class = "top"
+                    elif min_rank <= rank_threshold:
+                        rank_class = "high" 
+                    else:
+                        rank_class = ""
+                    
+                    if min_rank == max_rank:
+                        rank_text = str(min_rank)
+                    else:
+                        rank_text = f"{min_rank}-{max_rank}"
+                        
+                    html += f'<span class="rank-num {rank_class}">{rank_text}</span>'
+                
+                # 处理时间显示
+                time_display = title_data.get("time_display", "")
+                if time_display:
+                    # 简化时间显示格式,将波浪线替换为~
+                    simplified_time = time_display.replace(" ~ ", "~").replace("[", "").replace("]", "")
+                    html += f'<span class="time-info">{html_escape(simplified_time)}</span>'
+                
+                # 处理出现次数
+                count_info = title_data.get("count", 1)
+                if count_info > 1:
+                    html += f'<span class="count-info">{count_info}次</span>'
+                
+                html += """
+                            </div>
+                            <div class="news-title">"""
+                
+                # 处理标题和链接
+                escaped_title = html_escape(title_data["title"])
+                link_url = title_data.get("mobile_url") or title_data.get("url", "")
+                
+                if link_url:
+                    escaped_url = html_escape(link_url)
+                    html += f'<a href="{escaped_url}" target="_blank" class="news-link">{escaped_title}</a>'
+                else:
+                    html += escaped_title
+                
+                html += """
+                            </div>
+                        </div>
+                    </div>"""
 
-    html += """
-        </table>
-    """
+            html += """
+                </div>"""
 
+    # 处理新增新闻区域
     if report_data["new_titles"]:
         html += f"""
-        <div class="new-section">
-            <h3>🆕 本次新增热点新闻 (共 {report_data['total_new_count']} 条)</h3>
-        """
+                <div class="new-section">
+                    <div class="new-section-title">本次新增热点 (共 {report_data['total_new_count']} 条)</div>"""
 
         for source_data in report_data["new_titles"]:
             escaped_source = html_escape(source_data["source_name"])
-            html += f"<h4>{escaped_source} ({len(source_data['titles'])} 条)</h4><ul>"
-
-            for title_data in source_data["titles"]:
-                title_data_copy = title_data.copy()
-                title_data_copy["is_new"] = False
-                formatted_title = format_title_for_platform("html", title_data_copy)
-                if "] " in formatted_title:
-                    formatted_title = formatted_title.split("] ", 1)[1]
-                html += f"<li>{formatted_title}</li>"
+            titles_count = len(source_data["titles"])
+            
+            html += f"""
+                    <div class="new-source-group">
+                        <div class="new-source-title">{escaped_source} · {titles_count}条</div>"""
+
+            # 为新增新闻也添加序号
+            for idx, title_data in enumerate(source_data["titles"], 1):
+                ranks = title_data.get("ranks", [])
+                
+                # 处理新增新闻的排名显示
+                rank_class = ""
+                if ranks:
+                    min_rank = min(ranks)
+                    if min_rank <= 3:
+                        rank_class = "top"
+                    elif min_rank <= title_data.get("rank_threshold", 10):
+                        rank_class = "high"
+                    
+                    if len(ranks) == 1:
+                        rank_text = str(ranks[0])
+                    else:
+                        rank_text = f"{min(ranks)}-{max(ranks)}"
+                else:
+                    rank_text = "?"
+
+                html += f"""
+                        <div class="new-item">
+                            <div class="new-item-number">{idx}</div>
+                            <div class="new-item-rank {rank_class}">{rank_text}</div>
+                            <div class="new-item-content">
+                                <div class="new-item-title">"""
+                
+                # 处理新增新闻的链接
+                escaped_title = html_escape(title_data["title"])
+                link_url = title_data.get("mobile_url") or title_data.get("url", "")
+                
+                if link_url:
+                    escaped_url = html_escape(link_url)
+                    html += f'<a href="{escaped_url}" target="_blank" class="news-link">{escaped_title}</a>'
+                else:
+                    html += escaped_title
+                
+                html += """
+                                </div>
+                            </div>
+                        </div>"""
 
-            html += "</ul>"
+            html += """
+                    </div>"""
 
-        html += "</div>"
+        html += """
+                </div>"""
 
     html += """
+            </div>
+        </div>
     </body>
     </html>
     """

+ 11 - 6
readme.md

@@ -7,7 +7,7 @@
 [![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers)
 [![GitHub Forks](https://img.shields.io/github/forks/sansan0/TrendRadar?style=flat-square&logo=github&color=blue)](https://github.com/sansan0/TrendRadar/network/members)
 [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
-[![Version](https://img.shields.io/badge/version-v2.0.2-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
+[![Version](https://img.shields.io/badge/version-v2.0.3-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
 
 [![企业微信通知](https://img.shields.io/badge/企业微信-通知支持-00D4AA?style=flat-square)](https://work.weixin.qq.com/)
 [![Telegram通知](https://img.shields.io/badge/Telegram-通知支持-00D4AA?style=flat-square)](https://telegram.org/)
@@ -23,6 +23,8 @@
 > 本项目以轻量,易部署为目标,主要处理 issues
 >
 > 遇到问题提 issues,或【硅基茶水间】公众号留言
+>
+> **Fork** 的同时,如果觉得有帮助,请顺手点个 ⭐️ **Star**,感谢支持
 
 ## ✨ 核心功能
 
@@ -72,10 +74,10 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 
 **典型场景:** 股市投资监控、品牌舆情追踪、行业动态关注、生活资讯获取
 
-**💡 提示:** 想要网页版?进入你的仓库 Settings → Pages,启用 GitHub Pages。比如我这里[TrendRadar](https://sansan0.github.io/TrendRadar/)。
+**💡 提示:** 想要**实时更新**的网页版?进入你的仓库 Settings → Pages,启用 GitHub Pages。比如我这里[TrendRadar](https://sansan0.github.io/TrendRadar/)。
 
 <p align="center">
-  <img src="_image/2025-06-14.jpg" alt="最新推送效果" width="400"/>
+  <img src="_image/2025-06-14.jpg" alt="飞书推送效果" width="400"/>
 </p>
 
 <details>
@@ -118,14 +120,17 @@ GitHub 一键 Fork 即可使用,无需编程基础。
 - **小版本更新**:直接在 GitHub 网页编辑器中,用本项目的 `main.py` 代码替换你 fork 仓库中的对应文件 
 - **大版本升级**:从 v1.x 升级到 v2.0 建议删除现有 fork 后重新 fork,这样更省力且避免配置冲突
 
-### 2025/07/28 - v2.0.2
+### 2025/08/06 - v2.0.3
 
-- 重构代码
-- 解决版本号容易被遗漏修改的问题
+- 优化 github page 的网页版效果,方便移动端使用
 
 <details>
 <summary><strong>👉 历史更新</strong></summary>
 
+### 2025/07/28 - v2.0.2
+
+- 重构代码
+- 解决版本号容易被遗漏修改的问题
 
 ### 2025/07/27 - v2.0.1
 

+ 1 - 1
version

@@ -1 +1 @@
-2.0.2
+2.0.3